cxtms 1.9.13

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 (215) hide show
  1. package/README.md +384 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +4784 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/extractUtils.d.ts +11 -0
  7. package/dist/extractUtils.d.ts.map +1 -0
  8. package/dist/extractUtils.js +19 -0
  9. package/dist/extractUtils.js.map +1 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +11 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/types.d.ts +129 -0
  15. package/dist/types.d.ts.map +1 -0
  16. package/dist/types.js +6 -0
  17. package/dist/types.js.map +1 -0
  18. package/dist/utils/schemaLoader.d.ts +17 -0
  19. package/dist/utils/schemaLoader.d.ts.map +1 -0
  20. package/dist/utils/schemaLoader.js +134 -0
  21. package/dist/utils/schemaLoader.js.map +1 -0
  22. package/dist/validator.d.ts +72 -0
  23. package/dist/validator.d.ts.map +1 -0
  24. package/dist/validator.js +432 -0
  25. package/dist/validator.js.map +1 -0
  26. package/dist/workflowValidator.d.ts +103 -0
  27. package/dist/workflowValidator.d.ts.map +1 -0
  28. package/dist/workflowValidator.js +753 -0
  29. package/dist/workflowValidator.js.map +1 -0
  30. package/package.json +51 -0
  31. package/schemas/actions/all.json +27 -0
  32. package/schemas/actions/clipboard.json +46 -0
  33. package/schemas/actions/confirm.json +21 -0
  34. package/schemas/actions/consoleLog.json +16 -0
  35. package/schemas/actions/dialog.json +25 -0
  36. package/schemas/actions/fileDownload.json +16 -0
  37. package/schemas/actions/forEach.json +31 -0
  38. package/schemas/actions/if.json +12 -0
  39. package/schemas/actions/mutation.json +25 -0
  40. package/schemas/actions/navigate.json +18 -0
  41. package/schemas/actions/navigateBack.json +22 -0
  42. package/schemas/actions/navigateBackOrClose.json +21 -0
  43. package/schemas/actions/notification.json +19 -0
  44. package/schemas/actions/openBarcodeScanner.json +104 -0
  45. package/schemas/actions/query.json +32 -0
  46. package/schemas/actions/refresh.json +13 -0
  47. package/schemas/actions/resetDirtyState.json +22 -0
  48. package/schemas/actions/setFields.json +21 -0
  49. package/schemas/actions/setStore.json +13 -0
  50. package/schemas/actions/validateForm.json +15 -0
  51. package/schemas/actions/workflow.json +24 -0
  52. package/schemas/components/README.md +147 -0
  53. package/schemas/components/appComponent.json +58 -0
  54. package/schemas/components/barcodeScanner.json +69 -0
  55. package/schemas/components/button.json +123 -0
  56. package/schemas/components/calendar.json +489 -0
  57. package/schemas/components/card.json +176 -0
  58. package/schemas/components/collection.json +54 -0
  59. package/schemas/components/dataGrid.json +119 -0
  60. package/schemas/components/datasource.json +151 -0
  61. package/schemas/components/dropdown.json +57 -0
  62. package/schemas/components/field-collection.json +618 -0
  63. package/schemas/components/field.json +265 -0
  64. package/schemas/components/form.json +234 -0
  65. package/schemas/components/index.json +71 -0
  66. package/schemas/components/layout.json +69 -0
  67. package/schemas/components/module.json +167 -0
  68. package/schemas/components/navDropdown.json +36 -0
  69. package/schemas/components/navbar.json +78 -0
  70. package/schemas/components/navbarItem.json +28 -0
  71. package/schemas/components/navbarLink.json +36 -0
  72. package/schemas/components/row.json +31 -0
  73. package/schemas/components/slot.json +30 -0
  74. package/schemas/components/tab.json +34 -0
  75. package/schemas/components/tabs.json +35 -0
  76. package/schemas/components/timeline.json +172 -0
  77. package/schemas/components/timelineGrid.json +328 -0
  78. package/schemas/fields/README.md +66 -0
  79. package/schemas/fields/attachment.json +156 -0
  80. package/schemas/fields/autocomplete-googleplaces.json +130 -0
  81. package/schemas/fields/checkbox.json +82 -0
  82. package/schemas/fields/date.json +88 -0
  83. package/schemas/fields/datetime.json +75 -0
  84. package/schemas/fields/email.json +75 -0
  85. package/schemas/fields/index.json +53 -0
  86. package/schemas/fields/number.json +91 -0
  87. package/schemas/fields/password.json +70 -0
  88. package/schemas/fields/radio.json +94 -0
  89. package/schemas/fields/rangedatetime.json +56 -0
  90. package/schemas/fields/select-async.json +334 -0
  91. package/schemas/fields/select.json +115 -0
  92. package/schemas/fields/tel.json +79 -0
  93. package/schemas/fields/text.json +86 -0
  94. package/schemas/fields/textarea.json +95 -0
  95. package/schemas/fields/time.json +91 -0
  96. package/schemas/fields/url.json +74 -0
  97. package/schemas/schema.graphql +12248 -0
  98. package/schemas/schemas.json +610 -0
  99. package/schemas/workflows/activity.json +96 -0
  100. package/schemas/workflows/common/condition.json +48 -0
  101. package/schemas/workflows/common/expression.json +76 -0
  102. package/schemas/workflows/common/mapping.json +173 -0
  103. package/schemas/workflows/common/step.json +38 -0
  104. package/schemas/workflows/flow/aggregation.json +44 -0
  105. package/schemas/workflows/flow/entity.json +129 -0
  106. package/schemas/workflows/flow/state.json +105 -0
  107. package/schemas/workflows/flow/transition.json +143 -0
  108. package/schemas/workflows/input.json +122 -0
  109. package/schemas/workflows/output.json +61 -0
  110. package/schemas/workflows/schedule.json +26 -0
  111. package/schemas/workflows/tasks/accounting-transaction.json +95 -0
  112. package/schemas/workflows/tasks/action-event.json +65 -0
  113. package/schemas/workflows/tasks/all.json +152 -0
  114. package/schemas/workflows/tasks/appmodule.json +56 -0
  115. package/schemas/workflows/tasks/attachment.json +97 -0
  116. package/schemas/workflows/tasks/authentication.json +86 -0
  117. package/schemas/workflows/tasks/caching.json +68 -0
  118. package/schemas/workflows/tasks/charge.json +92 -0
  119. package/schemas/workflows/tasks/commodity.json +92 -0
  120. package/schemas/workflows/tasks/contact-address.json +72 -0
  121. package/schemas/workflows/tasks/contact-payment-method.json +72 -0
  122. package/schemas/workflows/tasks/contact.json +82 -0
  123. package/schemas/workflows/tasks/csv.json +81 -0
  124. package/schemas/workflows/tasks/document-render.json +105 -0
  125. package/schemas/workflows/tasks/document-send.json +84 -0
  126. package/schemas/workflows/tasks/edi.json +157 -0
  127. package/schemas/workflows/tasks/email-send.json +110 -0
  128. package/schemas/workflows/tasks/error.json +72 -0
  129. package/schemas/workflows/tasks/export.json +90 -0
  130. package/schemas/workflows/tasks/filetransfer.json +102 -0
  131. package/schemas/workflows/tasks/flow-transition.json +68 -0
  132. package/schemas/workflows/tasks/foreach.json +69 -0
  133. package/schemas/workflows/tasks/generic.json +47 -0
  134. package/schemas/workflows/tasks/graphql.json +78 -0
  135. package/schemas/workflows/tasks/httpRequest.json +161 -0
  136. package/schemas/workflows/tasks/import.json +64 -0
  137. package/schemas/workflows/tasks/inventory.json +67 -0
  138. package/schemas/workflows/tasks/job.json +88 -0
  139. package/schemas/workflows/tasks/log.json +73 -0
  140. package/schemas/workflows/tasks/map.json +58 -0
  141. package/schemas/workflows/tasks/movement.json +54 -0
  142. package/schemas/workflows/tasks/note.json +59 -0
  143. package/schemas/workflows/tasks/number.json +65 -0
  144. package/schemas/workflows/tasks/order-tracking-event.json +109 -0
  145. package/schemas/workflows/tasks/order.json +139 -0
  146. package/schemas/workflows/tasks/payment.json +85 -0
  147. package/schemas/workflows/tasks/pdf-document.json +60 -0
  148. package/schemas/workflows/tasks/postal-codes.json +92 -0
  149. package/schemas/workflows/tasks/resolve-timezone.json +65 -0
  150. package/schemas/workflows/tasks/setVariable.json +76 -0
  151. package/schemas/workflows/tasks/switch.json +75 -0
  152. package/schemas/workflows/tasks/template.json +73 -0
  153. package/schemas/workflows/tasks/tracking-event.json +137 -0
  154. package/schemas/workflows/tasks/transmission.json +185 -0
  155. package/schemas/workflows/tasks/unzip-file.json +68 -0
  156. package/schemas/workflows/tasks/user.json +70 -0
  157. package/schemas/workflows/tasks/validation.json +99 -0
  158. package/schemas/workflows/tasks/while.json +53 -0
  159. package/schemas/workflows/tasks/workflow-execute.json +82 -0
  160. package/schemas/workflows/trigger.json +90 -0
  161. package/schemas/workflows/variable.json +46 -0
  162. package/schemas/workflows/workflow.json +335 -0
  163. package/scripts/postinstall.js +291 -0
  164. package/scripts/setup-vscode.js +80 -0
  165. package/skills/cxtms-developer/SKILL.md +118 -0
  166. package/skills/cxtms-developer/ref-cli-auth.md +120 -0
  167. package/skills/cxtms-developer/ref-entity-accounting.md +180 -0
  168. package/skills/cxtms-developer/ref-entity-commodity.md +239 -0
  169. package/skills/cxtms-developer/ref-entity-contact.md +163 -0
  170. package/skills/cxtms-developer/ref-entity-geography.md +154 -0
  171. package/skills/cxtms-developer/ref-entity-job.md +77 -0
  172. package/skills/cxtms-developer/ref-entity-notification.md +85 -0
  173. package/skills/cxtms-developer/ref-entity-order-sub.md +160 -0
  174. package/skills/cxtms-developer/ref-entity-order.md +183 -0
  175. package/skills/cxtms-developer/ref-entity-organization.md +41 -0
  176. package/skills/cxtms-developer/ref-entity-rate.md +182 -0
  177. package/skills/cxtms-developer/ref-entity-shared.md +176 -0
  178. package/skills/cxtms-developer/ref-entity-warehouse.md +115 -0
  179. package/skills/cxtms-developer/ref-graphql-query.md +309 -0
  180. package/skills/cxtms-module-builder/SKILL.md +477 -0
  181. package/skills/cxtms-module-builder/ref-components-data.md +293 -0
  182. package/skills/cxtms-module-builder/ref-components-display.md +411 -0
  183. package/skills/cxtms-module-builder/ref-components-forms.md +369 -0
  184. package/skills/cxtms-module-builder/ref-components-interactive.md +317 -0
  185. package/skills/cxtms-module-builder/ref-components-layout.md +390 -0
  186. package/skills/cxtms-module-builder/ref-components-specialized.md +477 -0
  187. package/skills/cxtms-workflow-builder/SKILL.md +438 -0
  188. package/skills/cxtms-workflow-builder/ref-accounting.md +66 -0
  189. package/skills/cxtms-workflow-builder/ref-communication.md +169 -0
  190. package/skills/cxtms-workflow-builder/ref-entity.md +342 -0
  191. package/skills/cxtms-workflow-builder/ref-expressions-ncalc.md +128 -0
  192. package/skills/cxtms-workflow-builder/ref-expressions-template.md +161 -0
  193. package/skills/cxtms-workflow-builder/ref-filetransfer.md +80 -0
  194. package/skills/cxtms-workflow-builder/ref-flow.md +210 -0
  195. package/skills/cxtms-workflow-builder/ref-other.md +157 -0
  196. package/skills/cxtms-workflow-builder/ref-query.md +105 -0
  197. package/skills/cxtms-workflow-builder/ref-utilities.md +417 -0
  198. package/templates/module-configuration.yaml +44 -0
  199. package/templates/module-form.yaml +152 -0
  200. package/templates/module-grid.yaml +229 -0
  201. package/templates/module-select.yaml +139 -0
  202. package/templates/module.yaml +84 -0
  203. package/templates/workflow-api-tracking.yaml +189 -0
  204. package/templates/workflow-basic.yaml +76 -0
  205. package/templates/workflow-document.yaml +155 -0
  206. package/templates/workflow-entity-trigger.yaml +90 -0
  207. package/templates/workflow-ftp-edi.yaml +158 -0
  208. package/templates/workflow-ftp-tracking.yaml +161 -0
  209. package/templates/workflow-mcp-tool.yaml +112 -0
  210. package/templates/workflow-public-api.yaml +135 -0
  211. package/templates/workflow-scheduled-execute.yaml +75 -0
  212. package/templates/workflow-scheduled.yaml +125 -0
  213. package/templates/workflow-utility.yaml +96 -0
  214. package/templates/workflow-webhook.yaml +128 -0
  215. package/templates/workflow.yaml +140 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4784 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * CX Schema Validator CLI - Unified validation for YAML modules and workflows
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const http = __importStar(require("http"));
46
+ const https = __importStar(require("https"));
47
+ const crypto = __importStar(require("crypto"));
48
+ const os = __importStar(require("os"));
49
+ const chalk_1 = __importDefault(require("chalk"));
50
+ const yaml_1 = __importStar(require("yaml"));
51
+ const validator_1 = require("./validator");
52
+ const workflowValidator_1 = require("./workflowValidator");
53
+ const extractUtils_1 = require("./extractUtils");
54
+ // ============================================================================
55
+ // .env loader — load KEY=VALUE pairs from .env in CWD into process.env
56
+ // ============================================================================
57
+ function loadEnvFile() {
58
+ const envPath = path.join(process.cwd(), '.env');
59
+ if (!fs.existsSync(envPath))
60
+ return;
61
+ const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed || trimmed.startsWith('#'))
65
+ continue;
66
+ const eqIdx = trimmed.indexOf('=');
67
+ if (eqIdx < 1)
68
+ continue;
69
+ const key = trimmed.slice(0, eqIdx).trim();
70
+ let value = trimmed.slice(eqIdx + 1).trim();
71
+ // Strip surrounding quotes
72
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
73
+ value = value.slice(1, -1);
74
+ }
75
+ if (!process.env[key]) {
76
+ process.env[key] = value;
77
+ }
78
+ }
79
+ }
80
+ loadEnvFile();
81
+ // ============================================================================
82
+ // Constants
83
+ // ============================================================================
84
+ const VERSION = require('../package.json').version;
85
+ const PROGRAM_NAME = 'cxtms';
86
+ // ============================================================================
87
+ // Help Text
88
+ // ============================================================================
89
+ const HELP_TEXT = `
90
+ ${chalk_1.default.bold.cyan('╔═══════════════════════════════════════════════════════════════════════════╗')}
91
+ ${chalk_1.default.bold.cyan('║')} ${chalk_1.default.bold.white('CX SCHEMA VALIDATOR')} ${chalk_1.default.gray(`v${VERSION}`)} ${chalk_1.default.bold.cyan('║')}
92
+ ${chalk_1.default.bold.cyan('║')} ${chalk_1.default.gray('Unified validation for CargoXplorer YAML files')} ${chalk_1.default.bold.cyan('║')}
93
+ ${chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════════════════════╝')}
94
+
95
+ ${chalk_1.default.bold.yellow('DESCRIPTION:')}
96
+ Validates CargoXplorer YAML module and workflow files against JSON Schema
97
+ definitions. Provides detailed error feedback with examples and schema
98
+ references to help fix validation issues.
99
+
100
+ ${chalk_1.default.bold.yellow('USAGE:')}
101
+ ${chalk_1.default.cyan(PROGRAM_NAME)} [command] [options] <files...>
102
+
103
+ ${chalk_1.default.bold.yellow('COMMANDS:')}
104
+ ${chalk_1.default.green('validate')} Validate YAML file(s) ${chalk_1.default.gray('(default command)')}
105
+ ${chalk_1.default.green('report')} Generate validation report for multiple files
106
+ ${chalk_1.default.green('init')} Initialize a new CX project (app.yaml, folders, docs)
107
+ ${chalk_1.default.green('create')} Create a new module, workflow, or task-schema from template
108
+ ${chalk_1.default.green('extract')} Extract a component (and its routes) to another module
109
+ ${chalk_1.default.green('sync-schemas')} Regenerate all.json from task schema directory
110
+ ${chalk_1.default.green('install-skills')} Install Claude Code skills into project .claude/skills/
111
+ ${chalk_1.default.green('setup-claude')} Add CX project instructions to CLAUDE.md
112
+ ${chalk_1.default.green('update')} Update @cxtms/cx-schema to the latest version
113
+ ${chalk_1.default.green('login')} Login to a CX environment (OAuth2 + PKCE)
114
+ ${chalk_1.default.green('logout')} Logout from a CX environment
115
+ ${chalk_1.default.green('pat')} Manage personal access tokens (create, list, revoke)
116
+ ${chalk_1.default.green('orgs')} List, select, or set active organization
117
+ ${chalk_1.default.green('appmodule')} Manage app modules on a CX server (deploy, undeploy)
118
+ ${chalk_1.default.green('workflow')} Manage workflows on a CX server (deploy, undeploy, execute, logs, log)
119
+ ${chalk_1.default.green('publish')} Publish all modules and workflows to a CX server
120
+ ${chalk_1.default.green('app')} Manage app manifests (install/upgrade from git, release to git, list)
121
+ ${chalk_1.default.green('query')} Run a GraphQL query against the CX server
122
+ ${chalk_1.default.green('gql')} Explore GraphQL schema (types, queries, mutations)
123
+ ${chalk_1.default.green('schema')} Show JSON schema for a component or task
124
+ ${chalk_1.default.green('example')} Show example YAML for a component or task
125
+ ${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
126
+ ${chalk_1.default.green('version')} Show version number
127
+ ${chalk_1.default.green('help')} Show this help message
128
+
129
+ ${chalk_1.default.bold.yellow('OPTIONS:')}
130
+ ${chalk_1.default.green('-h, --help')} Show this help message
131
+ ${chalk_1.default.green('-v, --version')} Show version number
132
+ ${chalk_1.default.green('-t, --type <type>')} Validation type: ${chalk_1.default.cyan('module')}, ${chalk_1.default.cyan('workflow')}, or ${chalk_1.default.cyan('auto')} ${chalk_1.default.gray('(default: auto)')}
133
+ ${chalk_1.default.green('-f, --format <format>')} Output format: ${chalk_1.default.cyan('pretty')}, ${chalk_1.default.cyan('json')}, or ${chalk_1.default.cyan('compact')} ${chalk_1.default.gray('(default: pretty)')}
134
+ ${chalk_1.default.green('-s, --schemas <path>')} Path to schemas directory
135
+ ${chalk_1.default.green('--verbose')} Show detailed output with schema paths
136
+ ${chalk_1.default.green('--quiet')} Only show errors, suppress other output
137
+ ${chalk_1.default.green('-r, --report <file>')} Generate report to file (html, md, or json)
138
+ ${chalk_1.default.green('--report-format <fmt>')} Report format: ${chalk_1.default.cyan('html')}, ${chalk_1.default.cyan('markdown')}, or ${chalk_1.default.cyan('json')} ${chalk_1.default.gray('(default: auto from extension)')}
139
+ ${chalk_1.default.green('--template <name>')} Template variant for create command (e.g., ${chalk_1.default.cyan('basic')})
140
+ ${chalk_1.default.green('--feature <name>')} Place file under features/<name>/ instead of root
141
+ ${chalk_1.default.green('--options <json>')} JSON field definitions for create (inline or file path)
142
+ ${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
143
+ ${chalk_1.default.green('--to <file>')} Target file for extract command
144
+ ${chalk_1.default.green('--copy')} Copy component instead of moving (source unchanged, target gets higher priority)
145
+ ${chalk_1.default.green('--org <id>')} Organization ID for server commands
146
+ ${chalk_1.default.green('--vars <json>')} JSON variables for workflow execute
147
+ ${chalk_1.default.green('--from <date>')} Filter logs from date (YYYY-MM-DD)
148
+ ${chalk_1.default.green('--to <date>')} Filter logs to date (YYYY-MM-DD)
149
+ ${chalk_1.default.green('--output <file>')} Save workflow log to file (or -o)
150
+ ${chalk_1.default.green('--console')} Print workflow log to stdout
151
+ ${chalk_1.default.green('--json')} Download JSON log instead of text
152
+ ${chalk_1.default.green('-m, --message <msg>')} Release message for app release (required)
153
+ ${chalk_1.default.green('-b, --branch <branch>')} Branch override for app install/publish
154
+ ${chalk_1.default.green('--force')} Force install (even if same version) or publish all
155
+ ${chalk_1.default.green('--skip-changed')} Skip modules with unpublished changes during install
156
+
157
+ ${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
158
+ ${chalk_1.default.gray('# Validate a module YAML file')}
159
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} modules/countries-module.yaml`)}
160
+
161
+ ${chalk_1.default.gray('# Validate a workflow YAML file')}
162
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflows/my-workflow.yaml`)}
163
+
164
+ ${chalk_1.default.gray('# Auto-detect file type and validate')}
165
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} --type auto my-file.yaml`)}
166
+
167
+ ${chalk_1.default.gray('# Validate with custom schemas directory')}
168
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} --schemas ./custom-schemas file.yaml`)}
169
+
170
+ ${chalk_1.default.gray('# Output validation results as JSON')}
171
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} --format json file.yaml > results.json`)}
172
+
173
+ ${chalk_1.default.gray('# Validate multiple files')}
174
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} module1.yaml module2.yaml workflow1.yaml`)}
175
+
176
+ ${chalk_1.default.bold.yellow('PROJECT COMMANDS:')}
177
+ ${chalk_1.default.gray('# Initialize a new project')}
178
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} init`)}
179
+
180
+ ${chalk_1.default.gray('# Create a new module from template')}
181
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create module my-module`)}
182
+
183
+ ${chalk_1.default.gray('# Create a new workflow from template')}
184
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create workflow my-workflow`)}
185
+
186
+ ${chalk_1.default.gray('# Create from a specific template variant')}
187
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create workflow my-workflow --template basic`)}
188
+
189
+ ${chalk_1.default.gray('# Create inside a feature folder')}
190
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create workflow my-workflow --feature billing`)}
191
+
192
+ ${chalk_1.default.gray('# Create module with custom fields')}
193
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create module my-config --template configuration --options '[{"name":"host","type":"text"},{"name":"port","type":"number"}]'`)}
194
+
195
+ ${chalk_1.default.gray('# Create a task schema with pre-populated task enums')}
196
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create task-schema filetransfer --tasks "FileTransfer/Connect@1,FileTransfer/Disconnect@1"`)}
197
+
198
+ ${chalk_1.default.gray('# Sync all.json after manually adding/removing task schemas')}
199
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} sync-schemas`)}
200
+
201
+ ${chalk_1.default.gray('# Extract a component to a new module')}
202
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml`)}
203
+
204
+ ${chalk_1.default.bold.yellow('SCHEMA COMMANDS:')}
205
+ ${chalk_1.default.gray('# Show schema for a component')}
206
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema form`)}
207
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema dataGrid`)}
208
+
209
+ ${chalk_1.default.gray('# Show schema for a workflow task')}
210
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema foreach`)}
211
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema graphql`)}
212
+
213
+ ${chalk_1.default.gray('# Show example YAML for a component')}
214
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} example form`)}
215
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} example workflow`)}
216
+
217
+ ${chalk_1.default.gray('# List all available schemas')}
218
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} list`)}
219
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} list --type workflow`)}
220
+
221
+ ${chalk_1.default.bold.yellow('AUTH COMMANDS:')}
222
+ ${chalk_1.default.gray('# Login to a CX environment')}
223
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} login https://qa.storevista.acuitive.net`)}
224
+
225
+ ${chalk_1.default.gray('# Logout from current session')}
226
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} logout`)}
227
+
228
+ ${chalk_1.default.bold.yellow('PAT COMMANDS:')}
229
+ ${chalk_1.default.gray('# Check PAT token status and setup instructions')}
230
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat setup`)}
231
+
232
+ ${chalk_1.default.gray('# Create a new PAT token')}
233
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat create "my-token-name"`)}
234
+
235
+ ${chalk_1.default.gray('# List active PAT tokens')}
236
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat list`)}
237
+
238
+ ${chalk_1.default.gray('# Revoke a PAT token by ID')}
239
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat revoke <tokenId>`)}
240
+
241
+ ${chalk_1.default.bold.yellow('ORG COMMANDS:')}
242
+ ${chalk_1.default.gray('# List organizations on the server')}
243
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs list`)}
244
+
245
+ ${chalk_1.default.gray('# Interactively select an organization')}
246
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs select`)}
247
+
248
+ ${chalk_1.default.gray('# Set active organization by ID')}
249
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs use <orgId>`)}
250
+
251
+ ${chalk_1.default.gray('# Show current context (server, org, app)')}
252
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs use`)}
253
+
254
+ ${chalk_1.default.bold.yellow('APPMODULE COMMANDS:')}
255
+ ${chalk_1.default.gray('# Deploy a module YAML to the server (creates or updates)')}
256
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule deploy modules/my-module.yaml`)}
257
+
258
+ ${chalk_1.default.gray('# Deploy with explicit org ID')}
259
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule deploy modules/my-module.yaml --org 42`)}
260
+
261
+ ${chalk_1.default.gray('# Undeploy an app module by UUID')}
262
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule undeploy <appModuleId>`)}
263
+
264
+ ${chalk_1.default.bold.yellow('WORKFLOW COMMANDS:')}
265
+ ${chalk_1.default.gray('# Deploy a workflow YAML to the server (creates or updates)')}
266
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow deploy workflows/my-workflow.yaml`)}
267
+
268
+ ${chalk_1.default.gray('# Undeploy a workflow by UUID')}
269
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow undeploy <workflowId>`)}
270
+
271
+ ${chalk_1.default.gray('# Execute a workflow')}
272
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId|file.yaml>`)}
273
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId> --vars '{"city":"London"}'`)}
274
+
275
+ ${chalk_1.default.gray('# List execution logs for a workflow (sorted desc)')}
276
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <workflowId|file.yaml>`)}
277
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <workflowId> --from 2026-01-01 --to 2026-01-31`)}
278
+
279
+ ${chalk_1.default.gray('# Download a specific execution log')}
280
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId>`)} ${chalk_1.default.gray('# save txt log to temp dir')}
281
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --output log.txt`)} ${chalk_1.default.gray('# save to file')}
282
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --console`)} ${chalk_1.default.gray('# print to stdout')}
283
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --json`)} ${chalk_1.default.gray('# download JSON log (more detail)')}
284
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --json --console`)} ${chalk_1.default.gray('# JSON log to stdout')}
285
+
286
+ ${chalk_1.default.bold.yellow('PUBLISH COMMANDS:')}
287
+ ${chalk_1.default.gray('# Publish all modules and workflows from current project')}
288
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish`)}
289
+
290
+ ${chalk_1.default.gray('# Publish only a specific feature directory')}
291
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish --feature billing`)}
292
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish billing`)}
293
+
294
+ ${chalk_1.default.gray('# Publish with explicit org ID')}
295
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish --org 42`)}
296
+
297
+ ${chalk_1.default.bold.yellow('APP COMMANDS:')}
298
+ ${chalk_1.default.gray('# Install/refresh app from git repository into the CX server')}
299
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install`)}
300
+
301
+ ${chalk_1.default.gray('# Force reinstall even if same version')}
302
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install --force`)}
303
+
304
+ ${chalk_1.default.gray('# Install from a specific branch')}
305
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install --branch develop`)}
306
+
307
+ ${chalk_1.default.gray('# Install but skip modules that have local changes')}
308
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install --skip-changed`)}
309
+
310
+ ${chalk_1.default.gray('# Upgrade app from git (alias for install)')}
311
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app upgrade`)}
312
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app upgrade --force`)}
313
+
314
+ ${chalk_1.default.gray('# Release server changes to git (creates a PR) — message is required')}
315
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Add new shipping module"`)}
316
+
317
+ ${chalk_1.default.gray('# Release specific workflows and/or modules by YAML file')}
318
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Fix order workflow" workflows/my-workflow.yaml`)}
319
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Update billing" workflows/a.yaml modules/b.yaml`)}
320
+
321
+ ${chalk_1.default.gray('# Force release all modules and workflows')}
322
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Full republish" --force`)}
323
+
324
+ ${chalk_1.default.gray('# List installed app manifests on the server')}
325
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app list`)}
326
+
327
+ ${chalk_1.default.bold.yellow('QUERY COMMANDS:')}
328
+ ${chalk_1.default.gray('# Run an inline GraphQL query')}
329
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} query '{ organizations(take: 5) { items { organizationId companyName } } }'`)}
330
+
331
+ ${chalk_1.default.gray('# Run a query from a .graphql file')}
332
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} query my-query.graphql`)}
333
+
334
+ ${chalk_1.default.gray('# Pass variables as JSON')}
335
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} query my-query.graphql --vars '{"id": 42}'`)}
336
+
337
+ ${chalk_1.default.bold.yellow('GRAPHQL SCHEMA EXPLORATION:')}
338
+ ${chalk_1.default.gray('# List all queries, mutations, and types')}
339
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql queries`)}
340
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql mutations`)}
341
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql types`)}
342
+
343
+ ${chalk_1.default.gray('# Filter by name')}
344
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql types --filter audit`)}
345
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql queries --filter order`)}
346
+
347
+ ${chalk_1.default.gray('# Inspect a specific type')}
348
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql type OrderGqlDto`)}
349
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql type AuditChangeEntry`)}
350
+
351
+ ${chalk_1.default.bold.yellow('VALIDATION TYPES:')}
352
+ ${chalk_1.default.bold('module')} - CargoXplorer UI module definitions (components, routes, entities)
353
+ ${chalk_1.default.bold('workflow')} - CargoXplorer workflow definitions (activities, tasks, triggers)
354
+ ${chalk_1.default.bold('auto')} - Auto-detect based on file content (checks for 'workflow:' vs 'module:')
355
+
356
+ ${chalk_1.default.bold.yellow('OUTPUT FORMATS:')}
357
+ ${chalk_1.default.bold('pretty')} - Colorized, human-readable output with detailed error info
358
+ ${chalk_1.default.bold('json')} - JSON output suitable for CI/CD pipelines
359
+ ${chalk_1.default.bold('compact')} - Minimal output showing only pass/fail and error count
360
+
361
+ ${chalk_1.default.bold.yellow('EXIT CODES:')}
362
+ ${chalk_1.default.green('0')} - Validation passed (no errors)
363
+ ${chalk_1.default.red('1')} - Validation failed (errors found)
364
+ ${chalk_1.default.red('2')} - CLI error (invalid arguments, file not found, etc.)
365
+
366
+ ${chalk_1.default.bold.yellow('ENVIRONMENT VARIABLES:')}
367
+ ${chalk_1.default.green('CXTMS_AUTH')} - PAT token for authentication (skips OAuth login)
368
+ ${chalk_1.default.green('CXTMS_SERVER')} - Server URL when using PAT auth (or set \`server\` in app.yaml)
369
+ ${chalk_1.default.green('CX_SCHEMA_PATH')} - Default path to schemas directory
370
+ ${chalk_1.default.green('NO_COLOR')} - Disable colored output
371
+
372
+ ${chalk_1.default.bold.yellow('MORE INFORMATION:')}
373
+ Documentation: ${chalk_1.default.underline.cyan('https://github.com/cxtms/cx-schema')}
374
+ Report issues: ${chalk_1.default.underline.cyan('https://github.com/cxtms/cx-schema/issues')}
375
+ `;
376
+ const SCHEMA_HELP = `
377
+ ${chalk_1.default.bold.yellow('SCHEMA COMMAND')}
378
+
379
+ Show the JSON Schema definition for a component, field, action, or task.
380
+
381
+ ${chalk_1.default.bold.yellow('USAGE:')}
382
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema <name>`)}
383
+
384
+ ${chalk_1.default.bold.yellow('AVAILABLE SCHEMAS:')}
385
+
386
+ ${chalk_1.default.bold('Module Components:')}
387
+ form, dataGrid, layout, tabs, tab, field, button, collection,
388
+ dropdown, datasource, calendar, card, navbar, timeline
389
+
390
+ ${chalk_1.default.bold('Workflow Core:')}
391
+ workflow, activity, input, output, variable, trigger, schedule
392
+
393
+ ${chalk_1.default.bold('Workflow Tasks:')}
394
+ foreach, switch, while, validation, graphql, httpRequest,
395
+ setVariable, map, log, error, csv, export, template, import,
396
+ order, contact, contact-address, contact-payment-method,
397
+ commodity, job, attachment, charge, payment,
398
+ email-send, document-render, document-send, pdf-document,
399
+ accounting-transaction, number, workflow-execute,
400
+ filetransfer, caching, flow-transition, user, authentication,
401
+ edi, note, appmodule, action-event, inventory, movement
402
+
403
+ ${chalk_1.default.bold.yellow('EXAMPLES:')}
404
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema form`)}
405
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema foreach`)}
406
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} schema workflow`)}
407
+ `;
408
+ const LIST_HELP = `
409
+ ${chalk_1.default.bold.yellow('LIST COMMAND')}
410
+
411
+ List all available schemas for validation.
412
+
413
+ ${chalk_1.default.bold.yellow('USAGE:')}
414
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} list [options]`)}
415
+
416
+ ${chalk_1.default.bold.yellow('OPTIONS:')}
417
+ ${chalk_1.default.green('--type <type>')} Filter by type: module, workflow, or all ${chalk_1.default.gray('(default: all)')}
418
+
419
+ ${chalk_1.default.bold.yellow('EXAMPLES:')}
420
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} list`)}
421
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} list --type module`)}
422
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} list --type workflow`)}
423
+ `;
424
+ const INIT_HELP = `
425
+ ${chalk_1.default.bold.yellow('INIT COMMAND')}
426
+
427
+ Initialize a new CX project with app.yaml, folders, and documentation.
428
+
429
+ ${chalk_1.default.bold.yellow('USAGE:')}
430
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} init [name]`)}
431
+
432
+ ${chalk_1.default.bold.yellow('ARGUMENTS:')}
433
+ ${chalk_1.default.green('name')} App name ${chalk_1.default.gray('(default: current directory name)')}
434
+
435
+ ${chalk_1.default.bold.yellow('FILES CREATED:')}
436
+ ${chalk_1.default.green('app.yaml')} - Application manifest (name, description, version)
437
+ ${chalk_1.default.green('README.md')} - Project documentation
438
+ ${chalk_1.default.green('AGENTS.md')} - AI assistant instructions for validation
439
+
440
+ ${chalk_1.default.bold.yellow('DIRECTORIES:')}
441
+ ${chalk_1.default.green('modules/')} - UI module YAML definitions
442
+ ${chalk_1.default.green('workflows/')} - Workflow YAML definitions
443
+ ${chalk_1.default.green('features/')} - Feature-scoped modules and workflows
444
+
445
+ ${chalk_1.default.bold.yellow('EXAMPLES:')}
446
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} init`)} ${chalk_1.default.gray('# Use directory name')}
447
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} init my-app`)} ${chalk_1.default.gray('# Use custom name')}
448
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} init @cargox/my-app`)} ${chalk_1.default.gray('# Use scoped name')}
449
+ `;
450
+ const CREATE_HELP = `
451
+ ${chalk_1.default.bold.yellow('CREATE COMMAND')}
452
+
453
+ Create a new module or workflow from template.
454
+
455
+ ${chalk_1.default.bold.yellow('USAGE:')}
456
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create <type> <name>`)}
457
+
458
+ ${chalk_1.default.bold.yellow('TYPES:')}
459
+ ${chalk_1.default.green('module')} - Create a new UI module YAML file
460
+ ${chalk_1.default.green('workflow')} - Create a new workflow YAML file
461
+
462
+ ${chalk_1.default.bold.yellow('WORKFLOW TEMPLATES:')}
463
+ ${chalk_1.default.green('basic')} Minimal starting point (default if --template omitted uses full template)
464
+ ${chalk_1.default.green('entity-trigger')} React to entity changes (Before/After triggers)
465
+ ${chalk_1.default.green('document')} Generate PDF/Excel documents
466
+ ${chalk_1.default.green('scheduled')} Cron-based batch processing
467
+ ${chalk_1.default.green('scheduled-execute')} Schedule another workflow on cron
468
+ ${chalk_1.default.green('utility')} Reusable helper (called via Workflow/Execute)
469
+ ${chalk_1.default.green('webhook')} HTTP endpoint for external callers (anonymous, rate-limited)
470
+ ${chalk_1.default.green('public-api')} REST API endpoint with OpenAPI documentation
471
+ ${chalk_1.default.green('mcp-tool')} Expose workflow as MCP tool for AI agents
472
+ ${chalk_1.default.green('ftp-tracking')} Import tracking events from FTP
473
+ ${chalk_1.default.green('ftp-edi')} Import orders from FTP via EDI
474
+ ${chalk_1.default.green('api-tracking')} Fetch tracking from carrier API
475
+
476
+ ${chalk_1.default.bold.yellow('EXAMPLES:')}
477
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create module orders`)}
478
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create workflow invoice-generator`)}
479
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create workflow stripe-events --template webhook`)}
480
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create workflow get-order --template public-api`)}
481
+ `;
482
+ const EXTRACT_HELP = `
483
+ ${chalk_1.default.bold.yellow('EXTRACT COMMAND')}
484
+
485
+ Extract a component (and its routes) from one module into another.
486
+
487
+ ${chalk_1.default.bold.yellow('USAGE:')}
488
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file> [--copy]`)}
489
+
490
+ ${chalk_1.default.bold.yellow('OPTIONS:')}
491
+ ${chalk_1.default.green('--copy')} Copy instead of move. Source stays unchanged, target gets higher priority.
492
+
493
+ ${chalk_1.default.bold.yellow('WHAT GETS MOVED:')}
494
+ ${chalk_1.default.green('Component')} - The component matching the exact name
495
+ ${chalk_1.default.green('Routes')} - Any routes whose component field matches the name
496
+ ${chalk_1.default.gray('Permissions and entities are NOT moved')}
497
+
498
+ ${chalk_1.default.bold.yellow('PRIORITY (--copy mode):')}
499
+ When using --copy, the target module gets a priority higher than the source:
500
+ ${chalk_1.default.gray('Source priority 1 → Target priority 2')}
501
+ ${chalk_1.default.gray('Source no priority → Target priority 1')}
502
+
503
+ ${chalk_1.default.bold.yellow('EXAMPLES:')}
504
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml`)}
505
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/main.yaml Dashboard --to modules/dashboard.yaml`)}
506
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml --copy`)}
507
+ `;
508
+ // ============================================================================
509
+ // Templates
510
+ // ============================================================================
511
+ function generateAppYaml(name) {
512
+ const dirName = path.basename(process.cwd());
513
+ const appName = name || dirName;
514
+ const scopedName = appName.startsWith('@') ? appName : `@cargox/${appName}`;
515
+ return `id: "${generateUUID()}"
516
+ name: "${scopedName}"
517
+ description: ""
518
+ author: "CargoX"
519
+ version: "1.0.0"
520
+ repository: ""
521
+ `;
522
+ }
523
+ function generateReadme() {
524
+ return `# CargoXplorer Application
525
+
526
+ This project contains CargoXplorer modules and workflows.
527
+
528
+ ## Project Structure
529
+
530
+ \`\`\`
531
+ ├── app.yaml # Application manifest
532
+ ├── modules/ # UI module definitions
533
+ │ └── *.yaml
534
+ ├── workflows/ # Workflow definitions
535
+ │ └── *.yaml
536
+ ├── features/ # Feature-scoped modules and workflows
537
+ │ └── <feature>/
538
+ │ ├── modules/
539
+ │ └── workflows/
540
+ ├── README.md # This file
541
+ └── AGENTS.md # AI assistant instructions
542
+ \`\`\`
543
+
544
+ ## Validation
545
+
546
+ ### Install the validator
547
+
548
+ \`\`\`bash
549
+ npm install @cxtms/cx-schema
550
+ \`\`\`
551
+
552
+ ### Validate files
553
+
554
+ \`\`\`bash
555
+ # Validate all modules
556
+ npx cxtms modules/*.yaml
557
+
558
+ # Validate all workflows
559
+ npx cxtms workflows/*.yaml
560
+
561
+ # Validate with detailed output
562
+ npx cxtms --verbose modules/my-module.yaml
563
+
564
+ # Generate validation report
565
+ npx cxtms report modules/*.yaml workflows/*.yaml --report report.html
566
+ \`\`\`
567
+
568
+ ### Create new files
569
+
570
+ \`\`\`bash
571
+ # Create a new module
572
+ npx cxtms create module my-module
573
+
574
+ # Create a new workflow
575
+ npx cxtms create workflow my-workflow
576
+ \`\`\`
577
+
578
+ ### View schemas and examples
579
+
580
+ \`\`\`bash
581
+ # List available schemas
582
+ npx cxtms list
583
+
584
+ # View schema for a component
585
+ npx cxtms schema form
586
+
587
+ # View example YAML
588
+ npx cxtms example workflow
589
+ \`\`\`
590
+
591
+ ## Documentation
592
+
593
+ - [CX Schema CLI Documentation](https://docs.cargoxplorer.com/docs/documents/cx-schema-cli)
594
+ - [Module Development Guide](https://docs.cargoxplorer.com/docs/development/app-modules)
595
+ - [Workflow Development Guide](https://docs.cargoxplorer.com/docs/development/workflows)
596
+ `;
597
+ }
598
+ function generateAgentsMd() {
599
+ return `# AI Assistant Instructions for CargoXplorer Development
600
+
601
+ This file provides instructions for AI assistants (like Claude, GPT, Copilot) when working with this CargoXplorer project.
602
+
603
+ ## Validation Commands
604
+
605
+ When making changes to YAML files, always validate them:
606
+
607
+ \`\`\`bash
608
+ # Validate a specific module file
609
+ npx cxtms modules/<module-name>.yaml
610
+
611
+ # Validate a specific workflow file
612
+ npx cxtms workflows/<workflow-name>.yaml
613
+
614
+ # Validate all files with a report
615
+ npx cxtms report modules/*.yaml workflows/*.yaml --report validation-report.md
616
+ \`\`\`
617
+
618
+ ## Schema Reference
619
+
620
+ Before editing components or tasks, check the schema:
621
+
622
+ \`\`\`bash
623
+ # View schema for components
624
+ npx cxtms schema form
625
+ npx cxtms schema dataGrid
626
+ npx cxtms schema layout
627
+
628
+ # View schema for workflow tasks
629
+ npx cxtms schema foreach
630
+ npx cxtms schema graphql
631
+ npx cxtms schema switch
632
+ \`\`\`
633
+
634
+ ## Creating New Files
635
+
636
+ Use templates to create properly structured files:
637
+
638
+ \`\`\`bash
639
+ # Create a new module
640
+ npx cxtms create module <name>
641
+
642
+ # Create a new workflow
643
+ npx cxtms create workflow <name>
644
+
645
+ # Create from a specific template variant
646
+ npx cxtms create workflow <name> --template basic
647
+
648
+ # Create inside a feature folder (features/<name>/workflows/)
649
+ npx cxtms create workflow <name> --feature billing
650
+ \`\`\`
651
+
652
+ ## Module Structure
653
+
654
+ Modules contain UI component definitions:
655
+
656
+ - **Components**: form, dataGrid, layout, tabs, card, etc.
657
+ - **Fields**: text, number, select, date, checkbox, etc.
658
+ - **Actions**: navigate, mutation, query, setFields, etc.
659
+ - **Routes**: Define navigation paths
660
+
661
+ ## Workflow Structure
662
+
663
+ Workflows contain automation definitions:
664
+
665
+ - **workflow**: Metadata (workflowId, name, executionMode)
666
+ - **inputs/outputs**: Parameter definitions
667
+ - **variables**: Internal state
668
+ - **activities**: Ordered steps containing tasks
669
+ - **triggers**: Manual, Entity, or Scheduled triggers
670
+
671
+ ### Common Task Types
672
+
673
+ - **Control flow**: foreach, switch, while, validation
674
+ - **Data**: Query/GraphQL, Map@1, SetVariable@1
675
+ - **Entity operations**: Order/Create@1, Contact/Update@1, etc.
676
+ - **Communication**: Email/Send@1, Document/Render@1
677
+
678
+ ## Best Practices
679
+
680
+ 1. **Always validate** after making changes to YAML files
681
+ 2. **Use verbose mode** (\`--verbose\`) for detailed error information
682
+ 3. **Check schemas** before adding new properties
683
+ 4. **Use templates** when creating new files
684
+ 5. **Generate reports** for batch validation of multiple files
685
+ `;
686
+ }
687
+ function findTemplatesPath() {
688
+ // Check for templates in node_modules
689
+ const nodeModulesTemplates = path.join(process.cwd(), 'node_modules', '@cxtms/cx-schema', 'templates');
690
+ if (fs.existsSync(nodeModulesTemplates)) {
691
+ return nodeModulesTemplates;
692
+ }
693
+ // Check in package directory (for development)
694
+ const packageTemplates = path.join(__dirname, '../templates');
695
+ if (fs.existsSync(packageTemplates)) {
696
+ return packageTemplates;
697
+ }
698
+ return undefined;
699
+ }
700
+ function loadTemplate(templateName, variant) {
701
+ const templatesPath = findTemplatesPath();
702
+ if (!templatesPath) {
703
+ throw new Error('Could not find templates directory');
704
+ }
705
+ // Try variant-specific template first (e.g., workflow-basic.yaml)
706
+ if (variant) {
707
+ const variantFile = path.join(templatesPath, `${templateName}-${variant}.yaml`);
708
+ if (fs.existsSync(variantFile)) {
709
+ return fs.readFileSync(variantFile, 'utf-8');
710
+ }
711
+ throw new Error(`Template variant not found: ${templateName}-${variant}. Available templates: ${listTemplates(templatesPath, templateName)}`);
712
+ }
713
+ const templateFile = path.join(templatesPath, `${templateName}.yaml`);
714
+ if (!fs.existsSync(templateFile)) {
715
+ throw new Error(`Template not found: ${templateName}`);
716
+ }
717
+ return fs.readFileSync(templateFile, 'utf-8');
718
+ }
719
+ function listTemplates(templatesPath, type) {
720
+ const files = fs.readdirSync(templatesPath)
721
+ .filter(f => f.startsWith(`${type}-`) && f.endsWith('.yaml'))
722
+ .map(f => f.replace(`${type}-`, '').replace('.yaml', ''));
723
+ return files.length > 0 ? files.join(', ') : 'none';
724
+ }
725
+ function processTemplate(template, variables) {
726
+ let result = template;
727
+ // Replace all {{variableName}} placeholders (but not \{{...}} which are escaped)
728
+ for (const [key, value] of Object.entries(variables)) {
729
+ // Match {{key}} but not \{{key}}
730
+ const regex = new RegExp(`(?<!\\\\)\\{\\{${key}\\}\\}`, 'g');
731
+ result = result.replace(regex, value);
732
+ }
733
+ // Unescape \{{ to {{ (for runtime expressions like {{inputs.entityId}})
734
+ result = result.replace(/\\(\{\{)/g, '$1');
735
+ return result;
736
+ }
737
+ function generateTemplateContent(type, name, fileName, variant, createOptions) {
738
+ const template = loadTemplate(type, variant);
739
+ const displayName = name
740
+ .split('-')
741
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
742
+ .join(' ');
743
+ const variables = {
744
+ name,
745
+ displayName,
746
+ displayNameNoSpaces: displayName.replace(/\s/g, ''),
747
+ uuid: generateUUID(),
748
+ fileName: fileName.replace(/\\/g, '/')
749
+ };
750
+ let result = processTemplate(template, variables);
751
+ if (createOptions) {
752
+ result = applyCreateOptions(result, createOptions);
753
+ }
754
+ return result;
755
+ }
756
+ function generateUUID() {
757
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
758
+ const r = Math.random() * 16 | 0;
759
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
760
+ return v.toString(16);
761
+ });
762
+ }
763
+ function parseCreateOptions(optionsArg) {
764
+ const jsonStr = resolveOptionsJson(optionsArg);
765
+ let parsed;
766
+ try {
767
+ parsed = JSON.parse(jsonStr);
768
+ }
769
+ catch (e) {
770
+ throw new Error(`Invalid --options JSON: ${e.message}`);
771
+ }
772
+ let result;
773
+ if (Array.isArray(parsed)) {
774
+ result = { fields: parsed };
775
+ }
776
+ else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.fields)) {
777
+ result = { entityName: parsed.entityName, fields: parsed.fields };
778
+ }
779
+ else {
780
+ throw new Error('--options must be a JSON array of fields or an object with { entityName?, fields[] }');
781
+ }
782
+ for (const field of result.fields) {
783
+ if (!field.name)
784
+ throw new Error('Each field in --options must have a "name" property');
785
+ if (!field.type)
786
+ throw new Error(`Field "${field.name}" in --options must have a "type" property`);
787
+ }
788
+ return result;
789
+ }
790
+ function resolveOptionsJson(optionsArg) {
791
+ const trimmed = optionsArg.trim();
792
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
793
+ return trimmed;
794
+ }
795
+ // Treat as file path
796
+ if (fs.existsSync(trimmed)) {
797
+ return fs.readFileSync(trimmed, 'utf-8');
798
+ }
799
+ throw new Error(`Invalid --options: not valid JSON and file not found: ${trimmed}`);
800
+ }
801
+ function fieldNameToLabel(name) {
802
+ return name
803
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
804
+ .replace(/[-_]/g, ' ')
805
+ .replace(/\b\w/g, c => c.toUpperCase())
806
+ .trim();
807
+ }
808
+ function fieldTypeToSchemaType(fieldType) {
809
+ switch (fieldType) {
810
+ case 'number': return 'number';
811
+ case 'checkbox': return 'boolean';
812
+ default: return 'string';
813
+ }
814
+ }
815
+ function findFormComponents(obj) {
816
+ const forms = [];
817
+ if (!obj || typeof obj !== 'object')
818
+ return forms;
819
+ if (obj.component === 'form') {
820
+ forms.push(obj);
821
+ }
822
+ if (obj.layout) {
823
+ forms.push(...findFormComponents(obj.layout));
824
+ }
825
+ if (obj.children && Array.isArray(obj.children)) {
826
+ for (const child of obj.children) {
827
+ forms.push(...findFormComponents(child));
828
+ }
829
+ }
830
+ return forms;
831
+ }
832
+ function updateQueryFields(queryStr, fieldNames) {
833
+ // Replace the innermost { fieldList } in a GraphQL query with new field names
834
+ return queryStr.replace(/\{([^{}]+)\}/g, (match, inner) => {
835
+ const lines = inner.trim().split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
836
+ const allIdentifiers = lines.every((l) => /^[a-zA-Z_]\w*$/.test(l));
837
+ if (allIdentifiers && lines.length > 0) {
838
+ const newFields = fieldNames.map(f => ` ${f}`).join('\n');
839
+ return `{\n${newFields}\n }`;
840
+ }
841
+ return match;
842
+ });
843
+ }
844
+ function applyFieldsToForm(form, fields) {
845
+ // Generate children (field components)
846
+ form.children = fields.map(f => {
847
+ const props = {
848
+ label: { 'en-US': f.label || fieldNameToLabel(f.name) },
849
+ type: f.type
850
+ };
851
+ if (f.required)
852
+ props.required = true;
853
+ return { component: 'field', name: f.name, props };
854
+ });
855
+ if (!form.props)
856
+ return;
857
+ // Generate initialValues.append from defaults
858
+ const append = {};
859
+ for (const f of fields) {
860
+ if (f.default !== undefined) {
861
+ append[f.name] = f.default;
862
+ }
863
+ }
864
+ if (Object.keys(append).length > 0) {
865
+ if (!form.props.initialValues)
866
+ form.props.initialValues = {};
867
+ form.props.initialValues.append = append;
868
+ }
869
+ else if (form.props.initialValues?.append) {
870
+ delete form.props.initialValues.append;
871
+ }
872
+ // Generate validationSchema
873
+ const schema = {};
874
+ for (const f of fields) {
875
+ const entry = { type: fieldTypeToSchemaType(f.type) };
876
+ if (f.required)
877
+ entry.required = true;
878
+ schema[f.name] = entry;
879
+ }
880
+ form.props.validationSchema = schema;
881
+ // Update query field lists
882
+ const fieldNames = fields.map(f => f.name);
883
+ if (form.props.queries && Array.isArray(form.props.queries)) {
884
+ for (const q of form.props.queries) {
885
+ if (q.query?.command && typeof q.query.command === 'string') {
886
+ // Preserve 'id' in query if it was originally present and not in custom fields
887
+ const preserveId = !fieldNames.includes('id') && /\bid\b/.test(q.query.command);
888
+ const queryFieldNames = preserveId ? ['id', ...fieldNames] : fieldNames;
889
+ q.query.command = updateQueryFields(q.query.command, queryFieldNames);
890
+ }
891
+ }
892
+ }
893
+ }
894
+ function applyFieldsToConfiguration(layout, fields) {
895
+ // Configuration fields are stored under customValues, so prefix all field names
896
+ const configFields = fields.map(f => ({
897
+ component: 'field',
898
+ name: `customValues.${f.name}`,
899
+ props: {
900
+ type: f.type,
901
+ label: { 'en-US': f.label || fieldNameToLabel(f.name) },
902
+ ...(f.required ? { required: true } : {})
903
+ }
904
+ }));
905
+ if (!layout.children)
906
+ layout.children = [];
907
+ layout.children.push(...configFields);
908
+ // Update defaultValue in configurations if present
909
+ // (handled separately since configurations is a top-level key)
910
+ }
911
+ function findDataGridComponents(obj) {
912
+ const grids = [];
913
+ if (!obj || typeof obj !== 'object')
914
+ return grids;
915
+ if (obj.component === 'dataGrid') {
916
+ grids.push(obj);
917
+ }
918
+ if (obj.layout) {
919
+ grids.push(...findDataGridComponents(obj.layout));
920
+ }
921
+ if (obj.children && Array.isArray(obj.children)) {
922
+ for (const child of obj.children) {
923
+ grids.push(...findDataGridComponents(child));
924
+ }
925
+ }
926
+ return grids;
927
+ }
928
+ function fieldTypeToShowAs(fieldType) {
929
+ switch (fieldType) {
930
+ case 'date':
931
+ return { component: 'text', props: { value: `{{ format ${fieldType} L }}` } };
932
+ case 'number':
933
+ return { component: 'text', props: { value: `{{ ${fieldType} }}` } };
934
+ case 'checkbox':
935
+ return { component: 'Badges/StatusesBadge' };
936
+ default:
937
+ return null;
938
+ }
939
+ }
940
+ function buildColumnFromField(field) {
941
+ const col = {
942
+ name: field.name,
943
+ label: { 'en-US': field.label || fieldNameToLabel(field.name) }
944
+ };
945
+ const showAs = fieldTypeToShowAs(field.type);
946
+ if (showAs)
947
+ col.showAs = showAs;
948
+ return col;
949
+ }
950
+ function applyFieldsToGrid(grid, fields) {
951
+ if (!grid.props?.views || !Array.isArray(grid.props.views))
952
+ return;
953
+ const customColumns = fields.map(f => buildColumnFromField(f));
954
+ for (const view of grid.props.views) {
955
+ if (!view.columns || !Array.isArray(view.columns))
956
+ continue;
957
+ // Keep id (hidden) and system columns (created, lastModified), replace the rest
958
+ const idCols = view.columns.filter((c) => c.name === 'id');
959
+ const systemCols = view.columns.filter((c) => c.name === 'created' || c.name === 'lastModified');
960
+ view.columns = [...idCols, ...customColumns, ...systemCols];
961
+ }
962
+ }
963
+ function applyFieldsToEntities(doc, fields, entityName) {
964
+ if (!doc.entities || !Array.isArray(doc.entities))
965
+ return;
966
+ for (const entity of doc.entities) {
967
+ if (entityName) {
968
+ entity.name = entityName;
969
+ entity.displayName = { 'en-US': fieldNameToLabel(entityName) };
970
+ }
971
+ entity.fields = fields.map(f => ({
972
+ name: f.name,
973
+ displayName: { 'en-US': f.label || fieldNameToLabel(f.name) },
974
+ fieldType: f.type
975
+ }));
976
+ }
977
+ }
978
+ function applyEntityNameToGrid(grid, entityName) {
979
+ if (grid.props?.options) {
980
+ grid.props.options.rootEntityName = entityName;
981
+ }
982
+ }
983
+ function findSelectAsyncFields(obj) {
984
+ const selects = [];
985
+ if (!obj || typeof obj !== 'object')
986
+ return selects;
987
+ if (obj.component === 'field' && obj.props?.type === 'select-async') {
988
+ selects.push(obj);
989
+ }
990
+ if (obj.layout) {
991
+ selects.push(...findSelectAsyncFields(obj.layout));
992
+ }
993
+ if (obj.children && Array.isArray(obj.children)) {
994
+ for (const child of obj.children) {
995
+ selects.push(...findSelectAsyncFields(child));
996
+ }
997
+ }
998
+ return selects;
999
+ }
1000
+ function applyFieldsToSelectAsync(selectField, fields) {
1001
+ if (!selectField.props)
1002
+ return;
1003
+ const fieldNames = fields.map(f => f.name);
1004
+ // Update query field lists in GraphQL queries
1005
+ if (selectField.props.queries && Array.isArray(selectField.props.queries)) {
1006
+ for (const q of selectField.props.queries) {
1007
+ if (q.query?.command && typeof q.query.command === 'string') {
1008
+ q.query.command = updateQueryFields(q.query.command, fieldNames);
1009
+ }
1010
+ }
1011
+ }
1012
+ // Build itemLabelTemplate from fields (exclude id-like fields)
1013
+ const labelFields = fields.filter(f => !f.name.toLowerCase().endsWith('id'));
1014
+ if (labelFields.length > 0 && selectField.props.options) {
1015
+ const labelParts = labelFields.map(f => `{{${f.name}}}`);
1016
+ selectField.props.options.itemLabelTemplate = labelParts.join(' - ');
1017
+ }
1018
+ }
1019
+ function applyCreateOptions(content, optionsArg) {
1020
+ const opts = parseCreateOptions(optionsArg);
1021
+ const fields = opts.fields;
1022
+ // Extract comment header (lines before YAML content)
1023
+ const lines = content.split('\n');
1024
+ const headerLines = [];
1025
+ for (const line of lines) {
1026
+ if (line.startsWith('#') || line.trim() === '') {
1027
+ headerLines.push(line);
1028
+ }
1029
+ else {
1030
+ break;
1031
+ }
1032
+ }
1033
+ // Parse YAML
1034
+ const doc = yaml_1.default.parse(content);
1035
+ if (!doc)
1036
+ throw new Error('Failed to parse template YAML for --options processing');
1037
+ let applied = false;
1038
+ const isConfiguration = Array.isArray(doc.configurations);
1039
+ if (doc.components && Array.isArray(doc.components)) {
1040
+ for (const comp of doc.components) {
1041
+ // Apply to configuration templates (fields go directly into layout children)
1042
+ if (isConfiguration && comp.layout) {
1043
+ applyFieldsToConfiguration(comp.layout, fields);
1044
+ applied = true;
1045
+ continue;
1046
+ }
1047
+ // Apply to form components
1048
+ const forms = findFormComponents(comp);
1049
+ for (const form of forms) {
1050
+ applyFieldsToForm(form, fields);
1051
+ applied = true;
1052
+ }
1053
+ // Apply to dataGrid components (grid template)
1054
+ const grids = findDataGridComponents(comp);
1055
+ for (const grid of grids) {
1056
+ applyFieldsToGrid(grid, fields);
1057
+ if (opts.entityName) {
1058
+ applyEntityNameToGrid(grid, opts.entityName);
1059
+ }
1060
+ applied = true;
1061
+ }
1062
+ // Apply to select-async fields (select template)
1063
+ const selects = findSelectAsyncFields(comp);
1064
+ for (const sel of selects) {
1065
+ applyFieldsToSelectAsync(sel, fields);
1066
+ applied = true;
1067
+ }
1068
+ }
1069
+ }
1070
+ // Apply to entities
1071
+ if (doc.entities && Array.isArray(doc.entities)) {
1072
+ applyFieldsToEntities(doc, fields, opts.entityName);
1073
+ applied = true;
1074
+ }
1075
+ // Apply defaults to configuration defaultValue
1076
+ if (isConfiguration && doc.configurations) {
1077
+ for (const config of doc.configurations) {
1078
+ if (!config.defaultValue)
1079
+ config.defaultValue = {};
1080
+ for (const f of fields) {
1081
+ if (f.default !== undefined) {
1082
+ config.defaultValue[f.name] = f.default;
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ if (!applied) {
1088
+ console.warn(chalk_1.default.yellow('Warning: --options provided but no form or dataGrid component found in template'));
1089
+ return content;
1090
+ }
1091
+ // Dump back to YAML
1092
+ const yamlContent = yaml_1.default.stringify(doc, {
1093
+ indent: 2,
1094
+ lineWidth: 0,
1095
+ singleQuote: false,
1096
+ });
1097
+ return headerLines.join('\n') + yamlContent;
1098
+ }
1099
+ // ============================================================================
1100
+ // Init and Create Commands
1101
+ // ============================================================================
1102
+ function runInit(name) {
1103
+ console.log(chalk_1.default.bold.cyan('\n╔═══════════════════════════════════════════════════════════════════╗'));
1104
+ console.log(chalk_1.default.bold.cyan('║ CX PROJECT INITIALIZATION ║'));
1105
+ console.log(chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════════════╝\n'));
1106
+ const files = [
1107
+ { name: 'app.yaml', content: generateAppYaml(name || '') },
1108
+ { name: 'README.md', content: generateReadme() },
1109
+ { name: 'AGENTS.md', content: generateAgentsMd() }
1110
+ ];
1111
+ const createdDirs = [];
1112
+ const createdFiles = [];
1113
+ const skippedFiles = [];
1114
+ // Create directories
1115
+ for (const dir of ['modules', 'workflows', 'features']) {
1116
+ const dirPath = path.join(process.cwd(), dir);
1117
+ if (!fs.existsSync(dirPath)) {
1118
+ fs.mkdirSync(dirPath, { recursive: true });
1119
+ createdDirs.push(dir);
1120
+ }
1121
+ }
1122
+ // Create files
1123
+ for (const file of files) {
1124
+ const filePath = path.join(process.cwd(), file.name);
1125
+ if (fs.existsSync(filePath)) {
1126
+ skippedFiles.push(file.name);
1127
+ }
1128
+ else {
1129
+ fs.writeFileSync(filePath, file.content, 'utf-8');
1130
+ createdFiles.push(file.name);
1131
+ }
1132
+ }
1133
+ // Output summary
1134
+ if (createdDirs.length > 0) {
1135
+ console.log(chalk_1.default.bold(' Created directories:'));
1136
+ for (const dir of createdDirs) {
1137
+ console.log(chalk_1.default.green(` ✓ ${dir}/`));
1138
+ }
1139
+ console.log('');
1140
+ }
1141
+ if (createdFiles.length > 0) {
1142
+ console.log(chalk_1.default.bold(' Created files:'));
1143
+ for (const file of createdFiles) {
1144
+ console.log(chalk_1.default.green(` ✓ ${file}`));
1145
+ }
1146
+ console.log('');
1147
+ }
1148
+ if (skippedFiles.length > 0) {
1149
+ console.log(chalk_1.default.bold(' Skipped (already exist):'));
1150
+ for (const file of skippedFiles) {
1151
+ console.log(chalk_1.default.yellow(` - ${file}`));
1152
+ }
1153
+ console.log('');
1154
+ }
1155
+ // Setup CLAUDE.md with CX instructions
1156
+ runSetupClaude();
1157
+ console.log(chalk_1.default.gray(' Next steps:'));
1158
+ console.log(chalk_1.default.gray(` 1. Edit ${chalk_1.default.white('app.yaml')} to configure your project`));
1159
+ console.log(chalk_1.default.gray(` 2. Create modules: ${chalk_1.default.white(`${PROGRAM_NAME} create module <name>`)}`));
1160
+ console.log(chalk_1.default.gray(` 3. Create workflows: ${chalk_1.default.white(`${PROGRAM_NAME} create workflow <name>`)}`));
1161
+ console.log(chalk_1.default.gray(` 4. Validate files: ${chalk_1.default.white(`${PROGRAM_NAME} modules/*.yaml`)}`));
1162
+ console.log('');
1163
+ }
1164
+ function runCreate(type, name, template, feature, createOptions) {
1165
+ // Handle task-schema creation separately
1166
+ if (type === 'task-schema') {
1167
+ runCreateTaskSchema(name, createOptions);
1168
+ return;
1169
+ }
1170
+ if (!type || !['module', 'workflow'].includes(type)) {
1171
+ console.error(chalk_1.default.red('Error: Invalid or missing type. Use: module, workflow, or task-schema'));
1172
+ console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create module my-module`));
1173
+ process.exit(2);
1174
+ }
1175
+ if (!name) {
1176
+ console.error(chalk_1.default.red(`Error: Missing name for ${type}`));
1177
+ console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create ${type} my-${type}`));
1178
+ process.exit(2);
1179
+ }
1180
+ // Sanitize name: replace invalid chars with hyphen, collapse runs, trim edges
1181
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
1182
+ // Determine output directory and file
1183
+ // workflows/ or features/<feature>/workflows/ (same for modules)
1184
+ const baseDir = type === 'module' ? 'modules' : 'workflows';
1185
+ const dir = feature
1186
+ ? path.join('features', feature.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, ''), baseDir)
1187
+ : baseDir;
1188
+ const fileName = `${safeName}.yaml`;
1189
+ const filePath = path.join(process.cwd(), dir, fileName);
1190
+ // Check if file already exists
1191
+ if (fs.existsSync(filePath)) {
1192
+ console.error(chalk_1.default.red(`Error: File already exists: ${filePath}`));
1193
+ process.exit(2);
1194
+ }
1195
+ // Create directory if needed
1196
+ const dirPath = path.join(process.cwd(), dir);
1197
+ if (!fs.existsSync(dirPath)) {
1198
+ fs.mkdirSync(dirPath, { recursive: true });
1199
+ }
1200
+ // Generate content from template
1201
+ const relativeFileName = path.join(dir, fileName);
1202
+ let content;
1203
+ try {
1204
+ content = generateTemplateContent(type, safeName, relativeFileName, template, createOptions);
1205
+ }
1206
+ catch (error) {
1207
+ console.error(chalk_1.default.red(`Error loading template: ${error.message}`));
1208
+ process.exit(2);
1209
+ }
1210
+ // Write file
1211
+ fs.writeFileSync(filePath, content, 'utf-8');
1212
+ console.log(chalk_1.default.green(`\n✓ Created ${type}: ${path.join(dir, fileName)}`));
1213
+ console.log(chalk_1.default.gray(`\n Next steps:`));
1214
+ console.log(chalk_1.default.gray(` 1. Edit ${chalk_1.default.white(filePath)} to customize`));
1215
+ console.log(chalk_1.default.gray(` 2. Validate: ${chalk_1.default.white(`${PROGRAM_NAME} ${filePath}`)}`));
1216
+ console.log(chalk_1.default.gray(` 3. View schema: ${chalk_1.default.white(`${PROGRAM_NAME} schema ${type}`)}`));
1217
+ console.log('');
1218
+ }
1219
+ // ============================================================================
1220
+ // Create Task Schema Command
1221
+ // ============================================================================
1222
+ function runCreateTaskSchema(name, tasks) {
1223
+ if (!name) {
1224
+ console.error(chalk_1.default.red('Error: Missing name for task-schema'));
1225
+ console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create task-schema filetransfer --tasks "FileTransfer/Connect@1,FileTransfer/Disconnect@1"`));
1226
+ process.exit(2);
1227
+ }
1228
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
1229
+ // Find schemas directory — prefer source schemas/ in dev, fall back to standard resolution
1230
+ let schemasDir = path.join(process.cwd(), 'schemas');
1231
+ if (!fs.existsSync(schemasDir)) {
1232
+ const resolved = findSchemasPath();
1233
+ if (!resolved) {
1234
+ console.error(chalk_1.default.red('Error: Cannot find schemas directory'));
1235
+ process.exit(2);
1236
+ }
1237
+ schemasDir = resolved;
1238
+ }
1239
+ const tasksDir = path.join(schemasDir, 'workflows', 'tasks');
1240
+ if (!fs.existsSync(tasksDir)) {
1241
+ fs.mkdirSync(tasksDir, { recursive: true });
1242
+ }
1243
+ const filePath = path.join(tasksDir, `${safeName}.json`);
1244
+ if (fs.existsSync(filePath)) {
1245
+ console.error(chalk_1.default.red(`Error: Schema file already exists: ${filePath}`));
1246
+ process.exit(2);
1247
+ }
1248
+ // Parse --tasks flag (passed via --options)
1249
+ const taskEnums = [];
1250
+ if (tasks) {
1251
+ for (const t of tasks.split(',')) {
1252
+ const trimmed = t.trim();
1253
+ if (trimmed)
1254
+ taskEnums.push(trimmed);
1255
+ }
1256
+ }
1257
+ // Derive a title from the name
1258
+ const title = safeName
1259
+ .split('-')
1260
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
1261
+ .join(' ') + ' Tasks';
1262
+ // Build the schema JSON
1263
+ const schema = {
1264
+ $schema: 'http://json-schema.org/draft-07/schema#',
1265
+ title,
1266
+ description: `${title} operations`,
1267
+ type: 'object',
1268
+ properties: {
1269
+ task: {
1270
+ type: 'string',
1271
+ ...(taskEnums.length > 0 ? { enum: taskEnums } : {}),
1272
+ description: 'Task type identifier'
1273
+ },
1274
+ name: {
1275
+ type: 'string',
1276
+ description: 'Step name identifier'
1277
+ },
1278
+ description: {
1279
+ type: 'string',
1280
+ description: 'Step description'
1281
+ },
1282
+ conditions: {
1283
+ type: 'array',
1284
+ items: {
1285
+ type: 'object',
1286
+ properties: {
1287
+ expression: { type: 'string' }
1288
+ },
1289
+ required: ['expression']
1290
+ }
1291
+ },
1292
+ continueOnError: {
1293
+ type: 'boolean'
1294
+ },
1295
+ inputs: {
1296
+ type: 'object',
1297
+ description: `${title} inputs`,
1298
+ additionalProperties: true
1299
+ },
1300
+ outputs: {
1301
+ type: 'array',
1302
+ items: {
1303
+ type: 'object',
1304
+ properties: {
1305
+ name: { type: 'string' },
1306
+ mapping: { type: 'string' }
1307
+ },
1308
+ required: ['name', 'mapping']
1309
+ }
1310
+ }
1311
+ },
1312
+ required: ['task'],
1313
+ additionalProperties: true
1314
+ };
1315
+ fs.writeFileSync(filePath, JSON.stringify(schema, null, 2) + '\n', 'utf-8');
1316
+ // Sync all.json
1317
+ syncAllJson(tasksDir);
1318
+ // Invalidate cache so new schema is immediately discoverable
1319
+ _workflowTaskNamesCache = null;
1320
+ console.log(chalk_1.default.green(`\n✓ Created task schema: ${path.relative(process.cwd(), filePath)}`));
1321
+ console.log(chalk_1.default.gray(`\n Next steps:`));
1322
+ console.log(chalk_1.default.gray(` 1. Edit ${chalk_1.default.white(filePath)} to add typed input properties`));
1323
+ console.log(chalk_1.default.gray(` 2. Verify: ${chalk_1.default.white(`${PROGRAM_NAME} schema ${safeName}`)}`));
1324
+ console.log(chalk_1.default.gray(` 3. all.json has been auto-updated with the new reference`));
1325
+ console.log('');
1326
+ }
1327
+ // ============================================================================
1328
+ // Sync all.json (auto-regenerate $ref entries from task schema directory)
1329
+ // ============================================================================
1330
+ function syncAllJson(tasksDir) {
1331
+ const files = fs.readdirSync(tasksDir)
1332
+ .filter(f => f.endsWith('.json') && f !== 'all.json' && f !== 'generic.json')
1333
+ .sort();
1334
+ const anyOfRefs = files.map(f => ({ $ref: f }));
1335
+ // generic.json always last as fallback
1336
+ if (fs.existsSync(path.join(tasksDir, 'generic.json'))) {
1337
+ anyOfRefs.push({ $ref: 'generic.json' });
1338
+ }
1339
+ const allJson = {
1340
+ $schema: 'http://json-schema.org/draft-07/schema#',
1341
+ title: 'All Workflow Tasks',
1342
+ description: 'Aggregator schema for all workflow task types. Uses anyOf to allow matching any known task type or falling back to generic task structure.',
1343
+ type: 'object',
1344
+ anyOf: anyOfRefs
1345
+ };
1346
+ fs.writeFileSync(path.join(tasksDir, 'all.json'), JSON.stringify(allJson, null, 2) + '\n', 'utf-8');
1347
+ }
1348
+ function runSyncSchemas() {
1349
+ // Find schemas directory
1350
+ let schemasDir = path.join(process.cwd(), 'schemas');
1351
+ if (!fs.existsSync(schemasDir)) {
1352
+ const resolved = findSchemasPath();
1353
+ if (!resolved) {
1354
+ console.error(chalk_1.default.red('Error: Cannot find schemas directory'));
1355
+ process.exit(2);
1356
+ }
1357
+ schemasDir = resolved;
1358
+ }
1359
+ const tasksDir = path.join(schemasDir, 'workflows', 'tasks');
1360
+ if (!fs.existsSync(tasksDir)) {
1361
+ console.error(chalk_1.default.red('Error: Tasks directory not found'));
1362
+ process.exit(2);
1363
+ }
1364
+ syncAllJson(tasksDir);
1365
+ // Invalidate cache
1366
+ _workflowTaskNamesCache = null;
1367
+ const taskCount = fs.readdirSync(tasksDir)
1368
+ .filter(f => f.endsWith('.json') && f !== 'all.json' && f !== 'generic.json')
1369
+ .length;
1370
+ console.log(chalk_1.default.green(`\n✓ Synced all.json with ${taskCount} task schemas (+ generic fallback)`));
1371
+ console.log('');
1372
+ }
1373
+ // ============================================================================
1374
+ // Install Skills Command
1375
+ // ============================================================================
1376
+ function findPackageSkillsDir() {
1377
+ // Skills live in the package's skills/ directory
1378
+ // When running from dist/cli.js, the package root is one level up
1379
+ const packageRoot = path.resolve(__dirname, '..');
1380
+ const skillsDir = path.join(packageRoot, 'skills');
1381
+ if (fs.existsSync(skillsDir)) {
1382
+ return skillsDir;
1383
+ }
1384
+ return null;
1385
+ }
1386
+ function copyDirectorySync(src, dest) {
1387
+ if (!fs.existsSync(dest)) {
1388
+ fs.mkdirSync(dest, { recursive: true });
1389
+ }
1390
+ const entries = fs.readdirSync(src, { withFileTypes: true });
1391
+ for (const entry of entries) {
1392
+ const srcPath = path.join(src, entry.name);
1393
+ const destPath = path.join(dest, entry.name);
1394
+ if (entry.isDirectory()) {
1395
+ copyDirectorySync(srcPath, destPath);
1396
+ }
1397
+ else {
1398
+ fs.copyFileSync(srcPath, destPath);
1399
+ }
1400
+ }
1401
+ }
1402
+ function runInstallSkills() {
1403
+ console.log(chalk_1.default.bold.cyan('\n╔═══════════════════════════════════════════════════════════════════╗'));
1404
+ console.log(chalk_1.default.bold.cyan('║ INSTALL CLAUDE CODE SKILLS ║'));
1405
+ console.log(chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════════════╝\n'));
1406
+ const packageSkillsDir = findPackageSkillsDir();
1407
+ if (!packageSkillsDir) {
1408
+ console.error(chalk_1.default.red('Error: Could not find skills in the cx-schema package.'));
1409
+ process.exit(2);
1410
+ }
1411
+ const projectRoot = process.cwd();
1412
+ const skillNames = ['cxtms-developer', 'cxtms-module-builder', 'cxtms-workflow-builder'];
1413
+ let installed = 0;
1414
+ for (const skillName of skillNames) {
1415
+ const skillSource = path.join(packageSkillsDir, skillName);
1416
+ if (!fs.existsSync(skillSource)) {
1417
+ console.log(chalk_1.default.yellow(` Skipping ${skillName} (not found in package)`));
1418
+ continue;
1419
+ }
1420
+ const skillDest = path.join(projectRoot, '.claude', 'skills', skillName);
1421
+ console.log(` Installing ${chalk_1.default.cyan(skillName)}...`);
1422
+ // Remove existing skill directory to clean up stale files
1423
+ if (fs.existsSync(skillDest)) {
1424
+ fs.rmSync(skillDest, { recursive: true });
1425
+ }
1426
+ copyDirectorySync(skillSource, skillDest);
1427
+ installed++;
1428
+ }
1429
+ // Remove deprecated skills if they exist
1430
+ const deprecatedSkills = ['cx-build', 'cx-core', 'cx-module', 'cx-workflow'];
1431
+ for (const oldSkill of deprecatedSkills) {
1432
+ const oldSkillDest = path.join(projectRoot, '.claude', 'skills', oldSkill);
1433
+ if (fs.existsSync(oldSkillDest)) {
1434
+ fs.rmSync(oldSkillDest, { recursive: true });
1435
+ console.log(chalk_1.default.gray(` Removed deprecated ${oldSkill} skill.`));
1436
+ }
1437
+ }
1438
+ console.log('');
1439
+ console.log(chalk_1.default.green(`✓ Installed ${installed} skill(s) to .claude/skills/`));
1440
+ console.log('');
1441
+ }
1442
+ // ============================================================================
1443
+ // Update Command
1444
+ // ============================================================================
1445
+ function runUpdate() {
1446
+ console.log(chalk_1.default.bold.cyan('\n╔═══════════════════════════════════════════════════════════════════╗'));
1447
+ console.log(chalk_1.default.bold.cyan('║ UPDATE @cxtms/cx-schema ║'));
1448
+ console.log(chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════════════╝\n'));
1449
+ const { execSync } = require('child_process');
1450
+ console.log(' Updating to latest version...\n');
1451
+ try {
1452
+ execSync('npm install @cxtms/cx-schema@latest', {
1453
+ stdio: 'inherit',
1454
+ cwd: process.cwd()
1455
+ });
1456
+ // Read installed version from the updated package
1457
+ const installedPkgPath = path.join(process.cwd(), 'node_modules', '@cxtms', 'cx-schema', 'package.json');
1458
+ let installedVersion = 'unknown';
1459
+ if (fs.existsSync(installedPkgPath)) {
1460
+ installedVersion = JSON.parse(fs.readFileSync(installedPkgPath, 'utf-8')).version;
1461
+ }
1462
+ console.log('');
1463
+ console.log(chalk_1.default.green(`✓ @cxtms/cx-schema updated to v${installedVersion}`));
1464
+ }
1465
+ catch (error) {
1466
+ console.error(chalk_1.default.red('\nError: Failed to update @cxtms/cx-schema'));
1467
+ console.error(chalk_1.default.gray(error.message));
1468
+ process.exit(1);
1469
+ }
1470
+ // Reinstall skills and update CLAUDE.md (postinstall handles schemas)
1471
+ runInstallSkills();
1472
+ runSetupClaude();
1473
+ }
1474
+ // ============================================================================
1475
+ // Setup Claude Command
1476
+ // ============================================================================
1477
+ const CX_CLAUDE_MARKER = '<!-- cx-schema-instructions -->';
1478
+ function generateClaudeMdContent() {
1479
+ return `${CX_CLAUDE_MARKER}
1480
+ ## CargoXplorer Project
1481
+
1482
+ This is a CargoXplorer (CX) application. Modules and workflows are defined as YAML files validated against JSON schemas provided by \`@cxtms/cx-schema\`.
1483
+
1484
+ ### Project Structure
1485
+
1486
+ \`\`\`
1487
+ app.yaml # Application manifest (name, version, description)
1488
+ modules/ # UI module YAML files
1489
+ workflows/ # Workflow YAML files
1490
+ features/ # Feature-scoped modules and workflows
1491
+ <feature>/
1492
+ modules/
1493
+ workflows/
1494
+ \`\`\`
1495
+
1496
+ ### CLI — \`cxtms\`
1497
+
1498
+ **Always scaffold via CLI, never write YAML from scratch.**
1499
+
1500
+ | Command | Description |
1501
+ |---------|-------------|
1502
+ | \`npx cxtms create module <name>\` | Scaffold a UI module |
1503
+ | \`npx cxtms create workflow <name>\` | Scaffold a workflow |
1504
+ | \`npx cxtms create module <name> --template <t>\` | Use a specific template |
1505
+ | \`npx cxtms create workflow <name> --template <t>\` | Use a specific template |
1506
+ | \`npx cxtms create module <name> --feature <f>\` | Place under features/<f>/modules/ |
1507
+ | \`npx cxtms <file.yaml>\` | Validate a YAML file |
1508
+ | \`npx cxtms <file.yaml> --verbose\` | Validate with detailed errors |
1509
+ | \`npx cxtms schema <name>\` | Show JSON schema for a component or task |
1510
+ | \`npx cxtms example <name>\` | Show example YAML |
1511
+ | \`npx cxtms list\` | List all available schemas |
1512
+ | \`npx cxtms extract <src> <comp> --to <tgt>\` | Move component between modules |
1513
+
1514
+ **Module templates:** \`default\`, \`form\`, \`grid\`, \`select\`, \`configuration\`
1515
+ **Workflow templates:** \`basic\`, \`entity-trigger\`, \`document\`, \`scheduled\`, \`scheduled-execute\`, \`utility\`, \`webhook\`, \`public-api\`, \`mcp-tool\`, \`ftp-tracking\`, \`ftp-edi\`, \`api-tracking\`
1516
+
1517
+ ### Skills (slash commands)
1518
+
1519
+ | Skill | Purpose |
1520
+ |-------|---------|
1521
+ | \`/cxtms-module-builder <description>\` | Generate a UI module (forms, grids, screens) |
1522
+ | \`/cxtms-workflow-builder <description>\` | Generate a workflow (automation, triggers, integrations) |
1523
+ | \`/cxtms-developer <entity or question>\` | Look up entity fields, enums, and domain reference |
1524
+
1525
+ ### Workflow: Scaffold → Customize → Validate
1526
+
1527
+ 1. **Scaffold** — \`npx cxtms create module|workflow <name> --template <t>\`
1528
+ 2. **Read** the generated file
1529
+ 3. **Customize** for the use case
1530
+ 4. **Validate** — \`npx cxtms <file.yaml>\` — run after every change, fix all errors
1531
+ ${CX_CLAUDE_MARKER}`;
1532
+ }
1533
+ function runSetupClaude() {
1534
+ console.log(chalk_1.default.bold.cyan('\n╔═══════════════════════════════════════════════════════════════════╗'));
1535
+ console.log(chalk_1.default.bold.cyan('║ SETUP CLAUDE.md ║'));
1536
+ console.log(chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════════════╝\n'));
1537
+ const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
1538
+ const cxContent = generateClaudeMdContent();
1539
+ if (fs.existsSync(claudeMdPath)) {
1540
+ const existing = fs.readFileSync(claudeMdPath, 'utf-8');
1541
+ if (existing.includes(CX_CLAUDE_MARKER)) {
1542
+ // Replace existing CX section
1543
+ const markerRegex = new RegExp(CX_CLAUDE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
1544
+ '[\\s\\S]*?' +
1545
+ CX_CLAUDE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1546
+ const updated = existing.replace(markerRegex, cxContent);
1547
+ fs.writeFileSync(claudeMdPath, updated, 'utf-8');
1548
+ console.log(chalk_1.default.green(' ✓ Updated CX instructions in existing CLAUDE.md'));
1549
+ }
1550
+ else {
1551
+ // Append to existing file
1552
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
1553
+ fs.writeFileSync(claudeMdPath, existing + separator + cxContent + '\n', 'utf-8');
1554
+ console.log(chalk_1.default.green(' ✓ Appended CX instructions to existing CLAUDE.md'));
1555
+ }
1556
+ }
1557
+ else {
1558
+ // Create new file
1559
+ fs.writeFileSync(claudeMdPath, `# Project Instructions\n\n${cxContent}\n`, 'utf-8');
1560
+ console.log(chalk_1.default.green(' ✓ Created CLAUDE.md with CX instructions'));
1561
+ }
1562
+ console.log('');
1563
+ }
1564
+ // ============================================================================
1565
+ // Auth (Login / Logout)
1566
+ // ============================================================================
1567
+ const AUTH_CALLBACK_PORT = 9000;
1568
+ const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
1569
+ function getSessionDir() {
1570
+ const projectName = path.basename(process.cwd());
1571
+ return path.join(os.homedir(), '.cxtms', projectName);
1572
+ }
1573
+ function getSessionFilePath() {
1574
+ return path.join(getSessionDir(), '.session.json');
1575
+ }
1576
+ function readSessionFile() {
1577
+ const filePath = getSessionFilePath();
1578
+ if (!fs.existsSync(filePath))
1579
+ return null;
1580
+ try {
1581
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1582
+ }
1583
+ catch {
1584
+ return null;
1585
+ }
1586
+ }
1587
+ function writeSessionFile(data) {
1588
+ const dir = getSessionDir();
1589
+ if (!fs.existsSync(dir)) {
1590
+ fs.mkdirSync(dir, { recursive: true });
1591
+ }
1592
+ fs.writeFileSync(getSessionFilePath(), JSON.stringify(data, null, 2), 'utf-8');
1593
+ }
1594
+ function deleteSessionFile() {
1595
+ const filePath = getSessionFilePath();
1596
+ if (fs.existsSync(filePath)) {
1597
+ fs.unlinkSync(filePath);
1598
+ }
1599
+ }
1600
+ function generateCodeVerifier() {
1601
+ return crypto.randomBytes(32).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
1602
+ }
1603
+ function generateCodeChallenge(verifier) {
1604
+ return crypto.createHash('sha256').update(verifier).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
1605
+ }
1606
+ function httpsPost(url, body, contentType) {
1607
+ return new Promise((resolve, reject) => {
1608
+ const parsed = new URL(url);
1609
+ const isHttps = parsed.protocol === 'https:';
1610
+ const lib = isHttps ? https : http;
1611
+ const req = lib.request({
1612
+ hostname: parsed.hostname,
1613
+ port: parsed.port || (isHttps ? 443 : 80),
1614
+ path: parsed.pathname + parsed.search,
1615
+ method: 'POST',
1616
+ headers: {
1617
+ 'Content-Type': contentType,
1618
+ 'Content-Length': Buffer.byteLength(body),
1619
+ },
1620
+ }, (res) => {
1621
+ let data = '';
1622
+ res.on('data', (chunk) => data += chunk);
1623
+ res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
1624
+ });
1625
+ req.on('error', reject);
1626
+ req.write(body);
1627
+ req.end();
1628
+ });
1629
+ }
1630
+ function openBrowser(url) {
1631
+ const { exec } = require('child_process');
1632
+ const cmd = process.platform === 'win32' ? `start "" "${url}"`
1633
+ : process.platform === 'darwin' ? `open "${url}"`
1634
+ : `xdg-open "${url}"`;
1635
+ exec(cmd);
1636
+ }
1637
+ function startCallbackServer() {
1638
+ return new Promise((resolve, reject) => {
1639
+ const server = http.createServer((req, res) => {
1640
+ const reqUrl = new URL(req.url || '/', `http://127.0.0.1:${AUTH_CALLBACK_PORT}`);
1641
+ if (reqUrl.pathname === '/callback') {
1642
+ const code = reqUrl.searchParams.get('code');
1643
+ const error = reqUrl.searchParams.get('error');
1644
+ if (error) {
1645
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1646
+ res.end('<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>');
1647
+ reject(new Error(`OAuth error: ${error} - ${reqUrl.searchParams.get('error_description') || ''}`));
1648
+ server.close();
1649
+ return;
1650
+ }
1651
+ if (code) {
1652
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1653
+ res.end('<html><body><h2>Login successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
1654
+ resolve({ code, close: () => server.close() });
1655
+ return;
1656
+ }
1657
+ }
1658
+ res.writeHead(404);
1659
+ res.end();
1660
+ });
1661
+ server.on('error', (err) => {
1662
+ if (err.code === 'EADDRINUSE') {
1663
+ reject(new Error(`Port ${AUTH_CALLBACK_PORT} is already in use. Close the process using it and try again.`));
1664
+ }
1665
+ else {
1666
+ reject(err);
1667
+ }
1668
+ });
1669
+ server.listen(AUTH_CALLBACK_PORT, '127.0.0.1');
1670
+ });
1671
+ }
1672
+ async function registerOAuthClient(domain) {
1673
+ const res = await httpsPost(`${domain}/connect/register`, JSON.stringify({
1674
+ client_name: `cxtms-${crypto.randomBytes(4).toString('hex')}`,
1675
+ redirect_uris: [`http://localhost:${AUTH_CALLBACK_PORT}/callback`],
1676
+ grant_types: ['authorization_code', 'refresh_token'],
1677
+ response_types: ['code'],
1678
+ token_endpoint_auth_method: 'none',
1679
+ }), 'application/json');
1680
+ if (res.statusCode !== 200 && res.statusCode !== 201) {
1681
+ throw new Error(`Client registration failed (${res.statusCode}): ${res.body}`);
1682
+ }
1683
+ const data = JSON.parse(res.body);
1684
+ if (!data.client_id) {
1685
+ throw new Error('Client registration response missing client_id');
1686
+ }
1687
+ return data.client_id;
1688
+ }
1689
+ async function exchangeCodeForTokens(domain, clientId, code, codeVerifier) {
1690
+ const body = new URLSearchParams({
1691
+ grant_type: 'authorization_code',
1692
+ client_id: clientId,
1693
+ code,
1694
+ redirect_uri: `http://localhost:${AUTH_CALLBACK_PORT}/callback`,
1695
+ code_verifier: codeVerifier,
1696
+ }).toString();
1697
+ const res = await httpsPost(`${domain}/connect/token`, body, 'application/x-www-form-urlencoded');
1698
+ if (res.statusCode !== 200) {
1699
+ throw new Error(`Token exchange failed (${res.statusCode}): ${res.body}`);
1700
+ }
1701
+ const data = JSON.parse(res.body);
1702
+ return {
1703
+ domain,
1704
+ client_id: clientId,
1705
+ access_token: data.access_token,
1706
+ refresh_token: data.refresh_token,
1707
+ expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
1708
+ };
1709
+ }
1710
+ async function revokeToken(domain, clientId, token) {
1711
+ try {
1712
+ await httpsPost(`${domain}/connect/revoke`, new URLSearchParams({ client_id: clientId, token }).toString(), 'application/x-www-form-urlencoded');
1713
+ }
1714
+ catch {
1715
+ // Revocation failures are non-fatal
1716
+ }
1717
+ }
1718
+ async function refreshTokens(stored) {
1719
+ const body = new URLSearchParams({
1720
+ grant_type: 'refresh_token',
1721
+ client_id: stored.client_id,
1722
+ refresh_token: stored.refresh_token,
1723
+ }).toString();
1724
+ const res = await httpsPost(`${stored.domain}/connect/token`, body, 'application/x-www-form-urlencoded');
1725
+ if (res.statusCode !== 200) {
1726
+ throw new Error(`Token refresh failed (${res.statusCode}): ${res.body}`);
1727
+ }
1728
+ const data = JSON.parse(res.body);
1729
+ const updated = {
1730
+ ...stored,
1731
+ access_token: data.access_token,
1732
+ refresh_token: data.refresh_token || stored.refresh_token,
1733
+ expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
1734
+ };
1735
+ writeSessionFile(updated);
1736
+ return updated;
1737
+ }
1738
+ async function runLogin(domain) {
1739
+ // Normalize URL
1740
+ if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
1741
+ domain = `https://${domain}`;
1742
+ }
1743
+ domain = domain.replace(/\/+$/, '');
1744
+ try {
1745
+ new URL(domain);
1746
+ }
1747
+ catch {
1748
+ console.error(chalk_1.default.red('Error: Invalid URL'));
1749
+ process.exit(2);
1750
+ }
1751
+ console.log(chalk_1.default.bold.cyan('\n CX CLI Login\n'));
1752
+ // Step 1: Register client
1753
+ console.log(chalk_1.default.gray(' Registering OAuth client...'));
1754
+ const clientId = await registerOAuthClient(domain);
1755
+ console.log(chalk_1.default.green(' ✓ Client registered'));
1756
+ // Step 2: PKCE
1757
+ const codeVerifier = generateCodeVerifier();
1758
+ const codeChallenge = generateCodeChallenge(codeVerifier);
1759
+ // Step 3: Start callback server
1760
+ const callbackPromise = startCallbackServer();
1761
+ // Step 4: Open browser
1762
+ const authUrl = `${domain}/connect/authorize?` + new URLSearchParams({
1763
+ client_id: clientId,
1764
+ redirect_uri: `http://localhost:${AUTH_CALLBACK_PORT}/callback`,
1765
+ response_type: 'code',
1766
+ scope: 'openid offline_access TMS.ApiAPI',
1767
+ code_challenge: codeChallenge,
1768
+ code_challenge_method: 'S256',
1769
+ }).toString();
1770
+ console.log(chalk_1.default.gray(' Opening browser for login...'));
1771
+ openBrowser(authUrl);
1772
+ console.log(chalk_1.default.gray(` Waiting for login (timeout: 2 min)...`));
1773
+ // Step 5: Wait for callback with timeout
1774
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Login timed out after 2 minutes. Please try again.')), AUTH_TIMEOUT_MS));
1775
+ const { code, close } = await Promise.race([callbackPromise, timeoutPromise]);
1776
+ // Step 6: Exchange code for tokens
1777
+ console.log(chalk_1.default.gray(' Exchanging authorization code...'));
1778
+ const tokens = await exchangeCodeForTokens(domain, clientId, code, codeVerifier);
1779
+ // Step 7: Store session locally
1780
+ writeSessionFile(tokens);
1781
+ close();
1782
+ console.log(chalk_1.default.green(` ✓ Logged in to ${new URL(domain).hostname}`));
1783
+ console.log(chalk_1.default.gray(` Session stored at: ${getSessionFilePath()}\n`));
1784
+ }
1785
+ async function runLogout(_domain) {
1786
+ const session = readSessionFile();
1787
+ if (!session) {
1788
+ console.log(chalk_1.default.gray('\n No active session in this project.\n'));
1789
+ console.log(chalk_1.default.gray(` Login first: ${PROGRAM_NAME} login <url>\n`));
1790
+ return;
1791
+ }
1792
+ console.log(chalk_1.default.bold.cyan('\n CX CLI Logout\n'));
1793
+ console.log(chalk_1.default.gray(` Server: ${new URL(session.domain).hostname}`));
1794
+ // Revoke tokens (non-fatal)
1795
+ if (session.client_id && session.refresh_token) {
1796
+ console.log(chalk_1.default.gray(' Revoking tokens...'));
1797
+ await revokeToken(session.domain, session.client_id, session.access_token);
1798
+ await revokeToken(session.domain, session.client_id, session.refresh_token);
1799
+ }
1800
+ // Delete local session file
1801
+ deleteSessionFile();
1802
+ console.log(chalk_1.default.green(` ✓ Logged out from ${new URL(session.domain).hostname}\n`));
1803
+ }
1804
+ // ============================================================================
1805
+ // AppModule Commands
1806
+ // ============================================================================
1807
+ async function graphqlRequest(domain, token, query, variables) {
1808
+ const body = JSON.stringify({ query, variables });
1809
+ let res = await graphqlPostWithAuth(domain, token, body);
1810
+ if (res.statusCode === 401) {
1811
+ // PAT tokens have no refresh — fail immediately
1812
+ if (process.env.CXTMS_AUTH)
1813
+ throw new Error('PAT token unauthorized (401). Check your CXTMS_AUTH token.');
1814
+ // Try refresh for OAuth sessions
1815
+ const stored = readSessionFile();
1816
+ if (!stored)
1817
+ throw new Error('Session expired. Run `cxtms login <url>` again.');
1818
+ try {
1819
+ const refreshed = await refreshTokens(stored);
1820
+ res = await graphqlPostWithAuth(domain, refreshed.access_token, body);
1821
+ }
1822
+ catch {
1823
+ throw new Error('Session expired. Run `cxtms login <url>` again.');
1824
+ }
1825
+ }
1826
+ // Try to parse GraphQL errors from 400 responses too
1827
+ let json;
1828
+ try {
1829
+ json = JSON.parse(res.body);
1830
+ }
1831
+ catch {
1832
+ if (res.statusCode !== 200) {
1833
+ throw new Error(`GraphQL request failed (${res.statusCode}): ${res.body}`);
1834
+ }
1835
+ throw new Error('Invalid JSON response from GraphQL endpoint');
1836
+ }
1837
+ if (json.errors && json.errors.length > 0) {
1838
+ const messages = json.errors.map((e) => {
1839
+ const parts = [e.message];
1840
+ const ext = e.extensions?.message;
1841
+ if (ext && ext !== e.message)
1842
+ parts.push(ext);
1843
+ if (e.path)
1844
+ parts.push(`path: ${e.path.join('.')}`);
1845
+ return parts.join(' — ');
1846
+ });
1847
+ throw new Error(`GraphQL error: ${messages.join('; ')}`);
1848
+ }
1849
+ if (res.statusCode !== 200) {
1850
+ throw new Error(`GraphQL request failed (${res.statusCode}): ${res.body}`);
1851
+ }
1852
+ return json.data;
1853
+ }
1854
+ function graphqlPostWithAuth(domain, token, body) {
1855
+ return new Promise((resolve, reject) => {
1856
+ const url = `${domain}/api/graphql`;
1857
+ const parsed = new URL(url);
1858
+ const isHttps = parsed.protocol === 'https:';
1859
+ const lib = isHttps ? https : http;
1860
+ const req = lib.request({
1861
+ hostname: parsed.hostname,
1862
+ port: parsed.port || (isHttps ? 443 : 80),
1863
+ path: parsed.pathname + parsed.search,
1864
+ method: 'POST',
1865
+ headers: {
1866
+ 'Content-Type': 'application/json',
1867
+ 'Content-Length': Buffer.byteLength(body),
1868
+ 'Authorization': `Bearer ${token}`,
1869
+ },
1870
+ }, (res) => {
1871
+ let data = '';
1872
+ res.on('data', (chunk) => data += chunk);
1873
+ res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
1874
+ });
1875
+ req.on('error', reject);
1876
+ req.write(body);
1877
+ req.end();
1878
+ });
1879
+ }
1880
+ function resolveDomainFromAppYaml() {
1881
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
1882
+ if (!fs.existsSync(appYamlPath))
1883
+ return null;
1884
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
1885
+ const serverDomain = appYaml?.server || appYaml?.domain;
1886
+ if (!serverDomain)
1887
+ return null;
1888
+ let domain = serverDomain;
1889
+ if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
1890
+ domain = `https://${domain}`;
1891
+ }
1892
+ return domain.replace(/\/+$/, '');
1893
+ }
1894
+ function resolveSession() {
1895
+ // 0. Check for PAT token in env (CXTMS_AUTH) — skips OAuth entirely
1896
+ const patToken = process.env.CXTMS_AUTH;
1897
+ if (patToken) {
1898
+ const domain = process.env.CXTMS_SERVER ? process.env.CXTMS_SERVER.replace(/\/+$/, '') : resolveDomainFromAppYaml();
1899
+ if (!domain) {
1900
+ console.error(chalk_1.default.red('CXTMS_AUTH is set but no server domain found.'));
1901
+ console.error(chalk_1.default.gray('Add `server` to app.yaml or set CXTMS_SERVER in .env'));
1902
+ process.exit(2);
1903
+ }
1904
+ return {
1905
+ domain,
1906
+ client_id: '',
1907
+ access_token: patToken,
1908
+ refresh_token: '',
1909
+ expires_at: 0,
1910
+ };
1911
+ }
1912
+ // 1. Check local .cxtms/.session.json
1913
+ const session = readSessionFile();
1914
+ if (session)
1915
+ return session;
1916
+ // 2. Not logged in
1917
+ console.error(chalk_1.default.red('Not logged in. Run `cxtms login <url>` first.'));
1918
+ process.exit(2);
1919
+ }
1920
+ async function resolveOrgId(domain, token, override) {
1921
+ // 1. Explicit override
1922
+ if (override !== undefined)
1923
+ return override;
1924
+ // 2. Cached in session file
1925
+ const stored = readSessionFile();
1926
+ if (stored?.organization_id)
1927
+ return stored.organization_id;
1928
+ // 3. Query server
1929
+ const data = await graphqlRequest(domain, token, `
1930
+ query { organizations(take: 100) { items { organizationId companyName } } }
1931
+ `, {});
1932
+ const orgs = data?.organizations?.items;
1933
+ if (!orgs || orgs.length === 0) {
1934
+ throw new Error('No organizations found for this account.');
1935
+ }
1936
+ if (orgs.length === 1) {
1937
+ const orgId = orgs[0].organizationId;
1938
+ // Cache it
1939
+ if (stored) {
1940
+ stored.organization_id = orgId;
1941
+ writeSessionFile(stored);
1942
+ }
1943
+ return orgId;
1944
+ }
1945
+ // Multiple orgs — list and exit
1946
+ console.error(chalk_1.default.yellow('\n Multiple organizations found:\n'));
1947
+ for (const org of orgs) {
1948
+ console.error(chalk_1.default.white(` ${org.organizationId} ${org.companyName}`));
1949
+ }
1950
+ console.error(chalk_1.default.gray(`\n Run \`cxtms orgs select\` to choose, or pass --org <id>.\n`));
1951
+ process.exit(2);
1952
+ }
1953
+ async function runAppModuleDeploy(file, orgOverride) {
1954
+ if (!file) {
1955
+ console.error(chalk_1.default.red('Error: File path required'));
1956
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule deploy <file.yaml> [--org <id>]`));
1957
+ process.exit(2);
1958
+ }
1959
+ if (!fs.existsSync(file)) {
1960
+ console.error(chalk_1.default.red(`Error: File not found: ${file}`));
1961
+ process.exit(2);
1962
+ }
1963
+ const session = resolveSession();
1964
+ const domain = session.domain;
1965
+ const token = session.access_token;
1966
+ const orgId = await resolveOrgId(domain, token, orgOverride);
1967
+ // Read and parse YAML
1968
+ const yamlContent = fs.readFileSync(file, 'utf-8');
1969
+ const parsed = yaml_1.default.parse(yamlContent);
1970
+ const appModuleId = parsed?.module?.appModuleId;
1971
+ if (!appModuleId) {
1972
+ console.error(chalk_1.default.red('Error: Module YAML is missing module.appModuleId'));
1973
+ process.exit(2);
1974
+ }
1975
+ // Read app.yaml for appManifestId
1976
+ let appManifestId;
1977
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
1978
+ if (fs.existsSync(appYamlPath)) {
1979
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
1980
+ appManifestId = appYaml?.id;
1981
+ }
1982
+ console.log(chalk_1.default.bold.cyan('\n AppModule Deploy\n'));
1983
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
1984
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
1985
+ console.log(chalk_1.default.gray(` Module: ${appModuleId}`));
1986
+ console.log('');
1987
+ // Check if module exists
1988
+ const checkData = await graphqlRequest(domain, token, `
1989
+ query ($organizationId: Int!, $appModuleId: UUID!) {
1990
+ appModule(organizationId: $organizationId, appModuleId: $appModuleId) {
1991
+ appModuleId
1992
+ }
1993
+ }
1994
+ `, { organizationId: orgId, appModuleId });
1995
+ if (checkData?.appModule) {
1996
+ // Update
1997
+ console.log(chalk_1.default.gray(' Updating existing module...'));
1998
+ const updateValues = { appModuleYamlDocument: yamlContent };
1999
+ if (appManifestId)
2000
+ updateValues.appManifestId = appManifestId;
2001
+ const result = await graphqlRequest(domain, token, `
2002
+ mutation ($input: UpdateAppModuleInput!) {
2003
+ updateAppModule(input: $input) {
2004
+ appModule { appModuleId name }
2005
+ }
2006
+ }
2007
+ `, {
2008
+ input: {
2009
+ organizationId: orgId,
2010
+ appModuleId,
2011
+ values: updateValues,
2012
+ },
2013
+ });
2014
+ const mod = result?.updateAppModule?.appModule;
2015
+ console.log(chalk_1.default.green(` ✓ Updated: ${mod?.name || appModuleId}\n`));
2016
+ }
2017
+ else {
2018
+ // Create
2019
+ console.log(chalk_1.default.gray(' Creating new module...'));
2020
+ const values = { appModuleYamlDocument: yamlContent };
2021
+ if (appManifestId)
2022
+ values.appManifestId = appManifestId;
2023
+ const result = await graphqlRequest(domain, token, `
2024
+ mutation ($input: CreateAppModuleInput!) {
2025
+ createAppModule(input: $input) {
2026
+ appModule { appModuleId name }
2027
+ }
2028
+ }
2029
+ `, {
2030
+ input: {
2031
+ organizationId: orgId,
2032
+ values,
2033
+ },
2034
+ });
2035
+ const mod = result?.createAppModule?.appModule;
2036
+ console.log(chalk_1.default.green(` ✓ Created: ${mod?.name || appModuleId}\n`));
2037
+ }
2038
+ }
2039
+ async function runAppModuleUndeploy(uuid, orgOverride) {
2040
+ if (!uuid) {
2041
+ console.error(chalk_1.default.red('Error: AppModule ID required'));
2042
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule undeploy <appModuleId> [--org <id>]`));
2043
+ process.exit(2);
2044
+ }
2045
+ const session = resolveSession();
2046
+ const domain = session.domain;
2047
+ const token = session.access_token;
2048
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2049
+ console.log(chalk_1.default.bold.cyan('\n AppModule Undeploy\n'));
2050
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2051
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2052
+ console.log(chalk_1.default.gray(` Module: ${uuid}`));
2053
+ console.log('');
2054
+ await graphqlRequest(domain, token, `
2055
+ mutation ($input: DeleteAppModuleInput!) {
2056
+ deleteAppModule(input: $input) {
2057
+ deleteResult { __typename }
2058
+ }
2059
+ }
2060
+ `, {
2061
+ input: {
2062
+ organizationId: orgId,
2063
+ appModuleId: uuid,
2064
+ },
2065
+ });
2066
+ console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
2067
+ }
2068
+ async function runOrgsList() {
2069
+ const session = resolveSession();
2070
+ const domain = session.domain;
2071
+ const token = session.access_token;
2072
+ const data = await graphqlRequest(domain, token, `
2073
+ query { organizations(take: 100) { items { organizationId companyName } } }
2074
+ `, {});
2075
+ const orgs = data?.organizations?.items;
2076
+ if (!orgs || orgs.length === 0) {
2077
+ console.log(chalk_1.default.gray('\n No organizations found.\n'));
2078
+ return;
2079
+ }
2080
+ console.log(chalk_1.default.bold.cyan('\n Organizations\n'));
2081
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}\n`));
2082
+ for (const org of orgs) {
2083
+ const current = session.organization_id === org.organizationId;
2084
+ const marker = current ? chalk_1.default.green(' ← current') : '';
2085
+ console.log(chalk_1.default.white(` ${org.organizationId} ${org.companyName}${marker}`));
2086
+ }
2087
+ console.log('');
2088
+ }
2089
+ async function runOrgsUse(orgIdStr) {
2090
+ if (!orgIdStr) {
2091
+ // Show current context
2092
+ const session = resolveSession();
2093
+ const domain = session.domain;
2094
+ console.log(chalk_1.default.bold.cyan('\n Current Context\n'));
2095
+ console.log(chalk_1.default.white(` Server: ${new URL(domain).hostname}`));
2096
+ if (session.organization_id) {
2097
+ console.log(chalk_1.default.white(` Org: ${session.organization_id}`));
2098
+ }
2099
+ else {
2100
+ console.log(chalk_1.default.gray(` Org: (not set)`));
2101
+ }
2102
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
2103
+ if (fs.existsSync(appYamlPath)) {
2104
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
2105
+ if (appYaml?.id) {
2106
+ console.log(chalk_1.default.white(` App: ${appYaml.id} ${chalk_1.default.gray('(from app.yaml)')}`));
2107
+ }
2108
+ else {
2109
+ console.log(chalk_1.default.gray(` App: (not set)`));
2110
+ }
2111
+ }
2112
+ else {
2113
+ console.log(chalk_1.default.gray(` App: (not set)`));
2114
+ }
2115
+ console.log('');
2116
+ return;
2117
+ }
2118
+ const orgId = parseInt(orgIdStr, 10);
2119
+ if (isNaN(orgId)) {
2120
+ console.error(chalk_1.default.red(`Invalid organization ID: ${orgIdStr}. Must be a number.`));
2121
+ process.exit(2);
2122
+ }
2123
+ const session = resolveSession();
2124
+ const domain = session.domain;
2125
+ const token = session.access_token;
2126
+ // Validate the org exists
2127
+ const data = await graphqlRequest(domain, token, `
2128
+ query { organizations(take: 100) { items { organizationId companyName } } }
2129
+ `, {});
2130
+ const orgs = data?.organizations?.items;
2131
+ const match = orgs?.find((o) => o.organizationId === orgId);
2132
+ if (!match) {
2133
+ console.error(chalk_1.default.red(`Organization ${orgId} not found.`));
2134
+ if (orgs?.length) {
2135
+ console.error(chalk_1.default.gray('\n Available organizations:'));
2136
+ for (const org of orgs) {
2137
+ console.error(chalk_1.default.white(` ${org.organizationId} ${org.companyName}`));
2138
+ }
2139
+ }
2140
+ console.error('');
2141
+ process.exit(2);
2142
+ }
2143
+ // Save to session file
2144
+ session.organization_id = orgId;
2145
+ writeSessionFile(session);
2146
+ console.log(chalk_1.default.green(`\n ✓ Context set to: ${match.companyName} (${orgId})\n`));
2147
+ }
2148
+ async function runOrgsSelect() {
2149
+ const session = resolveSession();
2150
+ const domain = session.domain;
2151
+ const token = session.access_token;
2152
+ const data = await graphqlRequest(domain, token, `
2153
+ query { organizations(take: 100) { items { organizationId companyName } } }
2154
+ `, {});
2155
+ const orgs = data?.organizations?.items;
2156
+ if (!orgs || orgs.length === 0) {
2157
+ console.log(chalk_1.default.gray('\n No organizations found.\n'));
2158
+ return;
2159
+ }
2160
+ console.log(chalk_1.default.bold.cyan('\n Select Organization\n'));
2161
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}\n`));
2162
+ for (let i = 0; i < orgs.length; i++) {
2163
+ const org = orgs[i];
2164
+ const current = session.organization_id === org.organizationId;
2165
+ const marker = current ? chalk_1.default.green(' ← current') : '';
2166
+ console.log(chalk_1.default.white(` ${i + 1}) ${org.organizationId} ${org.companyName}${marker}`));
2167
+ }
2168
+ console.log('');
2169
+ const readline = require('readline');
2170
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2171
+ const answer = await new Promise((resolve) => {
2172
+ rl.question(chalk_1.default.yellow(' Enter number: '), (ans) => {
2173
+ rl.close();
2174
+ resolve(ans.trim());
2175
+ });
2176
+ });
2177
+ const idx = parseInt(answer, 10) - 1;
2178
+ if (isNaN(idx) || idx < 0 || idx >= orgs.length) {
2179
+ console.error(chalk_1.default.red('\n Invalid selection.\n'));
2180
+ process.exit(2);
2181
+ }
2182
+ const selected = orgs[idx];
2183
+ session.organization_id = selected.organizationId;
2184
+ writeSessionFile(session);
2185
+ console.log(chalk_1.default.green(`\n ✓ Context set to: ${selected.companyName} (${selected.organizationId})\n`));
2186
+ }
2187
+ // ============================================================================
2188
+ // Workflow Commands
2189
+ // ============================================================================
2190
+ async function runWorkflowDeploy(file, orgOverride) {
2191
+ if (!file) {
2192
+ console.error(chalk_1.default.red('Error: File path required'));
2193
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow deploy <file.yaml> [--org <id>]`));
2194
+ process.exit(2);
2195
+ }
2196
+ if (!fs.existsSync(file)) {
2197
+ console.error(chalk_1.default.red(`Error: File not found: ${file}`));
2198
+ process.exit(2);
2199
+ }
2200
+ const session = resolveSession();
2201
+ const domain = session.domain;
2202
+ const token = session.access_token;
2203
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2204
+ const yamlContent = fs.readFileSync(file, 'utf-8');
2205
+ const parsed = yaml_1.default.parse(yamlContent);
2206
+ const workflowId = parsed?.workflow?.workflowId;
2207
+ if (!workflowId) {
2208
+ console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2209
+ process.exit(2);
2210
+ }
2211
+ const workflowName = parsed?.workflow?.name || workflowId;
2212
+ // Read app.yaml for appManifestId
2213
+ let appManifestId;
2214
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
2215
+ if (fs.existsSync(appYamlPath)) {
2216
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
2217
+ appManifestId = appYaml?.id;
2218
+ }
2219
+ console.log(chalk_1.default.bold.cyan('\n Workflow Deploy\n'));
2220
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2221
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2222
+ console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
2223
+ console.log('');
2224
+ // Check if workflow exists
2225
+ const checkData = await graphqlRequest(domain, token, `
2226
+ query ($organizationId: Int!, $workflowId: UUID!) {
2227
+ workflow(organizationId: $organizationId, workflowId: $workflowId) {
2228
+ workflowId
2229
+ }
2230
+ }
2231
+ `, { organizationId: orgId, workflowId });
2232
+ if (checkData?.workflow) {
2233
+ console.log(chalk_1.default.gray(' Updating existing workflow...'));
2234
+ const updateInput = {
2235
+ organizationId: orgId,
2236
+ workflowId,
2237
+ workflowYamlDocument: yamlContent,
2238
+ };
2239
+ if (appManifestId)
2240
+ updateInput.appManifestId = appManifestId;
2241
+ const result = await graphqlRequest(domain, token, `
2242
+ mutation ($input: UpdateWorkflowInput!) {
2243
+ updateWorkflow(input: $input) {
2244
+ workflow { workflowId }
2245
+ }
2246
+ }
2247
+ `, {
2248
+ input: updateInput,
2249
+ });
2250
+ console.log(chalk_1.default.green(` ✓ Updated: ${workflowName}\n`));
2251
+ }
2252
+ else {
2253
+ console.log(chalk_1.default.gray(' Creating new workflow...'));
2254
+ const createInput = {
2255
+ organizationId: orgId,
2256
+ workflowYamlDocument: yamlContent,
2257
+ };
2258
+ if (appManifestId)
2259
+ createInput.appManifestId = appManifestId;
2260
+ const result = await graphqlRequest(domain, token, `
2261
+ mutation ($input: CreateWorkflowInput!) {
2262
+ createWorkflow(input: $input) {
2263
+ workflow { workflowId }
2264
+ }
2265
+ }
2266
+ `, {
2267
+ input: createInput,
2268
+ });
2269
+ console.log(chalk_1.default.green(` ✓ Created: ${workflowName}\n`));
2270
+ }
2271
+ }
2272
+ async function runWorkflowUndeploy(uuid, orgOverride) {
2273
+ if (!uuid) {
2274
+ console.error(chalk_1.default.red('Error: Workflow ID required'));
2275
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow undeploy <workflowId> [--org <id>]`));
2276
+ process.exit(2);
2277
+ }
2278
+ const session = resolveSession();
2279
+ const domain = session.domain;
2280
+ const token = session.access_token;
2281
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2282
+ console.log(chalk_1.default.bold.cyan('\n Workflow Undeploy\n'));
2283
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2284
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2285
+ console.log(chalk_1.default.gray(` Workflow: ${uuid}`));
2286
+ console.log('');
2287
+ await graphqlRequest(domain, token, `
2288
+ mutation ($input: DeleteWorkflowInput!) {
2289
+ deleteWorkflow(input: $input) {
2290
+ deleteResult { __typename }
2291
+ }
2292
+ }
2293
+ `, {
2294
+ input: {
2295
+ organizationId: orgId,
2296
+ workflowId: uuid,
2297
+ },
2298
+ });
2299
+ console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
2300
+ }
2301
+ async function uploadFileToServer(domain, token, orgId, localPath) {
2302
+ const fileName = path.basename(localPath);
2303
+ const ext = path.extname(localPath).toLowerCase();
2304
+ const contentTypeMap = {
2305
+ '.csv': 'text/csv', '.json': 'application/json', '.xml': 'application/xml',
2306
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
2307
+ '.xls': 'application/vnd.ms-excel', '.pdf': 'application/pdf',
2308
+ '.txt': 'text/plain', '.zip': 'application/zip',
2309
+ };
2310
+ const contentType = contentTypeMap[ext] || 'application/octet-stream';
2311
+ // Step 1: Get presigned upload URL
2312
+ const data = await graphqlRequest(domain, token, `
2313
+ query ($organizationId: Int!, $fileName: String!, $contentType: String!) {
2314
+ uploadUrl(organizationId: $organizationId, fileName: $fileName, contentType: $contentType) {
2315
+ presignedUrl
2316
+ fileUrl
2317
+ }
2318
+ }
2319
+ `, { organizationId: orgId, fileName, contentType });
2320
+ const presignedUrl = data?.uploadUrl?.presignedUrl;
2321
+ const fileUrl = data?.uploadUrl?.fileUrl;
2322
+ if (!presignedUrl || !fileUrl) {
2323
+ throw new Error('Failed to get upload URL from server');
2324
+ }
2325
+ // Step 2: PUT file content to presigned URL
2326
+ const fileContent = fs.readFileSync(localPath);
2327
+ const url = new URL(presignedUrl);
2328
+ const httpModule = url.protocol === 'https:' ? https : http;
2329
+ await new Promise((resolve, reject) => {
2330
+ const req = httpModule.request(url, {
2331
+ method: 'PUT',
2332
+ headers: {
2333
+ 'Content-Type': contentType,
2334
+ 'Content-Length': fileContent.length,
2335
+ 'x-ms-blob-type': 'BlockBlob',
2336
+ },
2337
+ }, (res) => {
2338
+ let body = '';
2339
+ res.on('data', (chunk) => body += chunk);
2340
+ res.on('end', () => {
2341
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
2342
+ resolve();
2343
+ }
2344
+ else {
2345
+ reject(new Error(`File upload failed (${res.statusCode}): ${body}`));
2346
+ }
2347
+ });
2348
+ });
2349
+ req.on('error', reject);
2350
+ req.write(fileContent);
2351
+ req.end();
2352
+ });
2353
+ return fileUrl;
2354
+ }
2355
+ async function runWorkflowExecute(workflowIdOrFile, orgOverride, variables, fileArgs) {
2356
+ if (!workflowIdOrFile) {
2357
+ console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
2358
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow execute <workflowId|file.yaml> [--org <id>] [--vars '{"key":"value"}'] [--file varName=path]`));
2359
+ process.exit(2);
2360
+ }
2361
+ const session = resolveSession();
2362
+ const { domain, access_token: token } = session;
2363
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2364
+ // Resolve workflowId
2365
+ let workflowId = workflowIdOrFile;
2366
+ let workflowName = workflowIdOrFile;
2367
+ if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
2368
+ if (!fs.existsSync(workflowIdOrFile)) {
2369
+ console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
2370
+ process.exit(2);
2371
+ }
2372
+ const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
2373
+ workflowId = parsed?.workflow?.workflowId;
2374
+ workflowName = parsed?.workflow?.name || path.basename(workflowIdOrFile);
2375
+ if (!workflowId) {
2376
+ console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2377
+ process.exit(2);
2378
+ }
2379
+ }
2380
+ // Parse variables if provided
2381
+ let vars;
2382
+ if (variables) {
2383
+ try {
2384
+ vars = JSON.parse(variables);
2385
+ }
2386
+ catch {
2387
+ console.error(chalk_1.default.red('Error: --vars must be valid JSON'));
2388
+ process.exit(2);
2389
+ }
2390
+ }
2391
+ // Process --file args: upload files and set URLs as variables
2392
+ if (fileArgs && fileArgs.length > 0) {
2393
+ if (!vars)
2394
+ vars = {};
2395
+ for (const fileArg of fileArgs) {
2396
+ const eqIdx = fileArg.indexOf('=');
2397
+ if (eqIdx < 1) {
2398
+ console.error(chalk_1.default.red(`Error: --file must be in format varName=path (got: ${fileArg})`));
2399
+ process.exit(2);
2400
+ }
2401
+ const varName = fileArg.substring(0, eqIdx);
2402
+ const filePath = fileArg.substring(eqIdx + 1);
2403
+ if (!fs.existsSync(filePath)) {
2404
+ console.error(chalk_1.default.red(`Error: File not found: ${filePath}`));
2405
+ process.exit(2);
2406
+ }
2407
+ console.log(chalk_1.default.gray(` Uploading ${path.basename(filePath)}...`));
2408
+ const fileUrl = await uploadFileToServer(domain, token, orgId, filePath);
2409
+ vars[varName] = fileUrl;
2410
+ console.log(chalk_1.default.gray(` → ${varName} = ${fileUrl}`));
2411
+ }
2412
+ }
2413
+ console.log(chalk_1.default.bold.cyan('\n Workflow Execute\n'));
2414
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2415
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2416
+ console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
2417
+ if (vars)
2418
+ console.log(chalk_1.default.gray(` Variables: ${JSON.stringify(vars)}`));
2419
+ console.log('');
2420
+ const input = { organizationId: orgId, workflowId };
2421
+ if (vars)
2422
+ input.variables = vars;
2423
+ const data = await graphqlRequest(domain, token, `
2424
+ mutation ($input: ExecuteWorkflowInput!) {
2425
+ executeWorkflow(input: $input) {
2426
+ workflowExecutionResult {
2427
+ executionId workflowId isAsync outputs
2428
+ }
2429
+ }
2430
+ }
2431
+ `, { input });
2432
+ const result = data?.executeWorkflow?.workflowExecutionResult;
2433
+ if (!result) {
2434
+ console.error(chalk_1.default.red(' No execution result returned.\n'));
2435
+ process.exit(2);
2436
+ }
2437
+ console.log(chalk_1.default.green(` ✓ Executed: ${workflowName}`));
2438
+ console.log(chalk_1.default.white(` Execution ID: ${result.executionId}`));
2439
+ console.log(chalk_1.default.white(` Async: ${result.isAsync}`));
2440
+ if (result.outputs && Object.keys(result.outputs).length > 0) {
2441
+ console.log(chalk_1.default.white(` Outputs:`));
2442
+ console.log(chalk_1.default.gray(` ${JSON.stringify(result.outputs, null, 2).split('\n').join('\n ')}`));
2443
+ }
2444
+ console.log('');
2445
+ }
2446
+ function resolveWorkflowId(workflowIdOrFile) {
2447
+ if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
2448
+ if (!fs.existsSync(workflowIdOrFile)) {
2449
+ console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
2450
+ process.exit(2);
2451
+ }
2452
+ const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
2453
+ const id = parsed?.workflow?.workflowId;
2454
+ if (!id) {
2455
+ console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2456
+ process.exit(2);
2457
+ }
2458
+ return id;
2459
+ }
2460
+ return workflowIdOrFile;
2461
+ }
2462
+ async function runWorkflowLogs(workflowIdOrFile, orgOverride, fromDate, toDate) {
2463
+ if (!workflowIdOrFile) {
2464
+ console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
2465
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow logs <workflowId|file.yaml> [--from <date>] [--to <date>]`));
2466
+ process.exit(2);
2467
+ }
2468
+ const session = resolveSession();
2469
+ const { domain, access_token: token } = session;
2470
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2471
+ const workflowId = resolveWorkflowId(workflowIdOrFile);
2472
+ // Parse date filters
2473
+ const fromTs = fromDate ? new Date(fromDate).getTime() : 0;
2474
+ const toTs = toDate ? new Date(toDate + 'T23:59:59').getTime() : Infinity;
2475
+ if (fromDate && isNaN(fromTs)) {
2476
+ console.error(chalk_1.default.red(`Invalid --from date: ${fromDate}. Use YYYY-MM-DD format.`));
2477
+ process.exit(2);
2478
+ }
2479
+ if (toDate && isNaN(toTs)) {
2480
+ console.error(chalk_1.default.red(`Invalid --to date: ${toDate}. Use YYYY-MM-DD format.`));
2481
+ process.exit(2);
2482
+ }
2483
+ const data = await graphqlRequest(domain, token, `
2484
+ query ($organizationId: Int!, $workflowId: UUID!) {
2485
+ workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 100) {
2486
+ totalCount
2487
+ items { executionId executionStatus executedAt durationMs txtLogUrl user { fullName email } }
2488
+ }
2489
+ }
2490
+ `, { organizationId: orgId, workflowId });
2491
+ let items = data?.workflowExecutions?.items || [];
2492
+ const total = data?.workflowExecutions?.totalCount || 0;
2493
+ // Filter by date range
2494
+ if (fromDate || toDate) {
2495
+ items = items.filter((ex) => {
2496
+ const t = new Date(ex.executedAt).getTime();
2497
+ return t >= fromTs && t <= toTs;
2498
+ });
2499
+ }
2500
+ // Sort descending
2501
+ items.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime());
2502
+ console.log(chalk_1.default.bold.cyan('\n Workflow Logs\n'));
2503
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2504
+ console.log(chalk_1.default.gray(` Workflow: ${workflowId}`));
2505
+ console.log(chalk_1.default.gray(` Total: ${total}`));
2506
+ if (fromDate || toDate) {
2507
+ console.log(chalk_1.default.gray(` Filter: ${fromDate || '...'} → ${toDate || '...'}`));
2508
+ }
2509
+ console.log(chalk_1.default.gray(` Showing: ${items.length}\n`));
2510
+ if (items.length === 0) {
2511
+ console.log(chalk_1.default.gray(' No executions found.\n'));
2512
+ return;
2513
+ }
2514
+ for (const ex of items) {
2515
+ const date = new Date(ex.executedAt).toLocaleString();
2516
+ const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
2517
+ const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
2518
+ const logIcon = ex.txtLogUrl ? chalk_1.default.green('●') : chalk_1.default.gray('○');
2519
+ const user = ex.user?.fullName || ex.user?.email || '';
2520
+ console.log(` ${logIcon} ${chalk_1.default.white(ex.executionId)} ${statusColor(ex.executionStatus.padEnd(10))} ${date} ${chalk_1.default.gray(duration)}${user ? ' ' + chalk_1.default.gray(user) : ''}`);
2521
+ }
2522
+ console.log();
2523
+ console.log(chalk_1.default.gray(` ${chalk_1.default.green('●')} log available ${chalk_1.default.gray('○')} no log`));
2524
+ console.log(chalk_1.default.gray(` Download: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console]\n`));
2525
+ }
2526
+ function fetchGzipText(url) {
2527
+ const zlib = require('zlib');
2528
+ return new Promise((resolve, reject) => {
2529
+ const lib = url.startsWith('https') ? https : http;
2530
+ lib.get(url, (res) => {
2531
+ if (res.statusCode !== 200) {
2532
+ reject(new Error(`HTTP ${res.statusCode}`));
2533
+ res.resume();
2534
+ return;
2535
+ }
2536
+ const rawChunks = [];
2537
+ res.on('data', (chunk) => rawChunks.push(chunk));
2538
+ res.on('end', () => {
2539
+ const raw = Buffer.concat(rawChunks);
2540
+ if (raw.length === 0) {
2541
+ resolve('(empty log)');
2542
+ return;
2543
+ }
2544
+ zlib.gunzip(raw, (err, result) => {
2545
+ if (err) {
2546
+ resolve(raw.toString('utf-8'));
2547
+ return;
2548
+ }
2549
+ resolve(result.toString('utf-8'));
2550
+ });
2551
+ });
2552
+ }).on('error', reject);
2553
+ });
2554
+ }
2555
+ async function runWorkflowLog(executionId, orgOverride, outputFile, toConsole, useJson) {
2556
+ if (!executionId) {
2557
+ console.error(chalk_1.default.red('Error: Execution ID required'));
2558
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console] [--json]`));
2559
+ process.exit(2);
2560
+ }
2561
+ const session = resolveSession();
2562
+ const { domain, access_token: token } = session;
2563
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2564
+ const data = await graphqlRequest(domain, token, `
2565
+ query ($organizationId: Int!, $executionId: UUID!) {
2566
+ workflowExecution(organizationId: $organizationId, executionId: $executionId) {
2567
+ executionId workflowId executionStatus executedAt durationMs
2568
+ txtLogUrl jsonLogUrl
2569
+ user { fullName email }
2570
+ }
2571
+ }
2572
+ `, { organizationId: orgId, executionId });
2573
+ const ex = data?.workflowExecution;
2574
+ if (!ex) {
2575
+ console.error(chalk_1.default.red(`Execution not found: ${executionId}`));
2576
+ process.exit(2);
2577
+ }
2578
+ const logUrl = useJson ? ex.jsonLogUrl : ex.txtLogUrl;
2579
+ const logType = useJson ? 'json' : 'txt';
2580
+ const ext = useJson ? '.json' : '.log';
2581
+ if (!logUrl) {
2582
+ console.error(chalk_1.default.yellow(`No ${logType} log available for this execution.`));
2583
+ process.exit(0);
2584
+ }
2585
+ const date = new Date(ex.executedAt).toLocaleString();
2586
+ const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
2587
+ const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
2588
+ const userName = ex.user?.fullName || ex.user?.email || '';
2589
+ // Download log
2590
+ let logText;
2591
+ try {
2592
+ logText = await fetchGzipText(logUrl);
2593
+ }
2594
+ catch (e) {
2595
+ console.error(chalk_1.default.red(`Failed to download log: ${e.message}`));
2596
+ process.exit(2);
2597
+ }
2598
+ // Pretty-print JSON if it's valid JSON
2599
+ if (useJson) {
2600
+ try {
2601
+ const parsed = JSON.parse(logText);
2602
+ logText = JSON.stringify(parsed, null, 2);
2603
+ }
2604
+ catch { /* keep as-is */ }
2605
+ }
2606
+ if (toConsole) {
2607
+ console.log(chalk_1.default.bold.cyan('\n Workflow Execution\n'));
2608
+ console.log(chalk_1.default.white(` ID: ${ex.executionId}`));
2609
+ console.log(chalk_1.default.white(` Workflow: ${ex.workflowId}`));
2610
+ console.log(chalk_1.default.white(` Status: ${statusColor(ex.executionStatus)}`));
2611
+ console.log(chalk_1.default.white(` Executed: ${date}`));
2612
+ console.log(chalk_1.default.white(` Duration: ${duration}`));
2613
+ if (userName)
2614
+ console.log(chalk_1.default.white(` User: ${userName}`));
2615
+ console.log(chalk_1.default.gray(`\n --- ${logType.toUpperCase()} Log ---\n`));
2616
+ console.log(logText);
2617
+ return;
2618
+ }
2619
+ // Save to file
2620
+ let filePath;
2621
+ if (outputFile) {
2622
+ filePath = path.resolve(outputFile);
2623
+ }
2624
+ else {
2625
+ const tmpDir = os.tmpdir();
2626
+ const dateStr = new Date(ex.executedAt).toISOString().slice(0, 10);
2627
+ filePath = path.join(tmpDir, `workflow-${ex.workflowId}-${dateStr}-${executionId}${ext}`);
2628
+ }
2629
+ fs.writeFileSync(filePath, logText, 'utf-8');
2630
+ console.log(chalk_1.default.green(` ✓ ${logType.toUpperCase()} log saved: ${filePath}`));
2631
+ console.log(chalk_1.default.gray(` Execution: ${executionId} ${statusColor(ex.executionStatus)} ${date} ${duration}`));
2632
+ }
2633
+ // ============================================================================
2634
+ // Publish Command
2635
+ // ============================================================================
2636
+ async function pushWorkflowQuiet(domain, token, orgId, file, appManifestId) {
2637
+ let name = path.basename(file);
2638
+ try {
2639
+ const yamlContent = fs.readFileSync(file, 'utf-8');
2640
+ const parsed = yaml_1.default.parse(yamlContent);
2641
+ const workflowId = parsed?.workflow?.workflowId;
2642
+ name = parsed?.workflow?.name || name;
2643
+ if (!workflowId)
2644
+ return { ok: false, name, error: 'Missing workflow.workflowId' };
2645
+ const checkData = await graphqlRequest(domain, token, `
2646
+ query ($organizationId: Int!, $workflowId: UUID!) {
2647
+ workflow(organizationId: $organizationId, workflowId: $workflowId) { workflowId }
2648
+ }
2649
+ `, { organizationId: orgId, workflowId });
2650
+ if (checkData?.workflow) {
2651
+ const updateInput = { organizationId: orgId, workflowId, workflowYamlDocument: yamlContent };
2652
+ if (appManifestId)
2653
+ updateInput.appManifestId = appManifestId;
2654
+ await graphqlRequest(domain, token, `
2655
+ mutation ($input: UpdateWorkflowInput!) {
2656
+ updateWorkflow(input: $input) { workflow { workflowId } }
2657
+ }
2658
+ `, { input: updateInput });
2659
+ }
2660
+ else {
2661
+ const createInput = { organizationId: orgId, workflowYamlDocument: yamlContent };
2662
+ if (appManifestId)
2663
+ createInput.appManifestId = appManifestId;
2664
+ await graphqlRequest(domain, token, `
2665
+ mutation ($input: CreateWorkflowInput!) {
2666
+ createWorkflow(input: $input) { workflow { workflowId } }
2667
+ }
2668
+ `, { input: createInput });
2669
+ }
2670
+ return { ok: true, name };
2671
+ }
2672
+ catch (e) {
2673
+ return { ok: false, name, error: e.message };
2674
+ }
2675
+ }
2676
+ async function pushModuleQuiet(domain, token, orgId, file, appManifestId) {
2677
+ let name = path.basename(file);
2678
+ try {
2679
+ const yamlContent = fs.readFileSync(file, 'utf-8');
2680
+ const parsed = yaml_1.default.parse(yamlContent);
2681
+ const appModuleId = parsed?.module?.appModuleId;
2682
+ name = parsed?.module?.name || name;
2683
+ if (!appModuleId)
2684
+ return { ok: false, name, error: 'Missing module.appModuleId' };
2685
+ const checkData = await graphqlRequest(domain, token, `
2686
+ query ($organizationId: Int!, $appModuleId: UUID!) {
2687
+ appModule(organizationId: $organizationId, appModuleId: $appModuleId) { appModuleId }
2688
+ }
2689
+ `, { organizationId: orgId, appModuleId });
2690
+ if (checkData?.appModule) {
2691
+ const updateValues = { appModuleYamlDocument: yamlContent };
2692
+ if (appManifestId)
2693
+ updateValues.appManifestId = appManifestId;
2694
+ await graphqlRequest(domain, token, `
2695
+ mutation ($input: UpdateAppModuleInput!) {
2696
+ updateAppModule(input: $input) { appModule { appModuleId name } }
2697
+ }
2698
+ `, { input: { organizationId: orgId, appModuleId, values: updateValues } });
2699
+ }
2700
+ else {
2701
+ const values = { appModuleYamlDocument: yamlContent };
2702
+ if (appManifestId)
2703
+ values.appManifestId = appManifestId;
2704
+ await graphqlRequest(domain, token, `
2705
+ mutation ($input: CreateAppModuleInput!) {
2706
+ createAppModule(input: $input) { appModule { appModuleId name } }
2707
+ }
2708
+ `, { input: { organizationId: orgId, values } });
2709
+ }
2710
+ return { ok: true, name };
2711
+ }
2712
+ catch (e) {
2713
+ return { ok: false, name, error: e.message };
2714
+ }
2715
+ }
2716
+ // ============================================================================
2717
+ // PAT Token Commands
2718
+ // ============================================================================
2719
+ async function runPatCreate(name) {
2720
+ const session = resolveSession();
2721
+ const { domain, access_token: token } = session;
2722
+ const data = await graphqlRequest(domain, token, `
2723
+ mutation ($input: CreatePersonalAccessTokenInput!) {
2724
+ createPersonalAccessToken(input: $input) {
2725
+ createPatPayload {
2726
+ token
2727
+ personalAccessToken { id name scopes }
2728
+ }
2729
+ }
2730
+ }
2731
+ `, { input: { input: { name, scopes: ['TMS.ApiAPI'] } } });
2732
+ const payload = data?.createPersonalAccessToken?.createPatPayload;
2733
+ const patToken = payload?.token;
2734
+ const pat = payload?.personalAccessToken;
2735
+ if (!patToken) {
2736
+ console.error(chalk_1.default.red('Failed to create PAT token — no token returned.'));
2737
+ process.exit(2);
2738
+ }
2739
+ console.log(chalk_1.default.green('PAT token created successfully!'));
2740
+ console.log();
2741
+ console.log(chalk_1.default.bold(' Token:'), chalk_1.default.cyan(patToken));
2742
+ console.log(chalk_1.default.bold(' ID: '), chalk_1.default.gray(pat?.id || 'unknown'));
2743
+ console.log(chalk_1.default.bold(' Name: '), pat?.name || name);
2744
+ console.log();
2745
+ console.log(chalk_1.default.yellow('⚠ Copy the token now — it will not be shown again.'));
2746
+ console.log();
2747
+ console.log(chalk_1.default.bold('To use PAT authentication, add to your project .env file:'));
2748
+ console.log();
2749
+ console.log(chalk_1.default.cyan(` CXTMS_AUTH=${patToken}`));
2750
+ console.log(chalk_1.default.cyan(` CXTMS_SERVER=${domain}`));
2751
+ console.log();
2752
+ console.log(chalk_1.default.gray('When CXTMS_AUTH is set, cxtms will skip OAuth login and use the PAT token directly.'));
2753
+ console.log(chalk_1.default.gray('You can also export these as environment variables instead of using .env.'));
2754
+ }
2755
+ async function runPatList() {
2756
+ const session = resolveSession();
2757
+ const { domain, access_token: token } = session;
2758
+ const data = await graphqlRequest(domain, token, `
2759
+ {
2760
+ personalAccessTokens(skip: 0, take: 50) {
2761
+ items { id name createdAt expiresAt lastUsedAt scopes }
2762
+ totalCount
2763
+ }
2764
+ }
2765
+ `, {});
2766
+ const items = data?.personalAccessTokens?.items || [];
2767
+ const total = data?.personalAccessTokens?.totalCount ?? items.length;
2768
+ if (items.length === 0) {
2769
+ console.log(chalk_1.default.gray('No active PAT tokens found.'));
2770
+ return;
2771
+ }
2772
+ console.log(chalk_1.default.bold(`PAT tokens (${total}):\n`));
2773
+ for (const t of items) {
2774
+ const expires = t.expiresAt ? new Date(t.expiresAt).toLocaleDateString() : 'never';
2775
+ const lastUsed = t.lastUsedAt ? new Date(t.lastUsedAt).toLocaleDateString() : 'never';
2776
+ console.log(` ${chalk_1.default.cyan(t.name || '(unnamed)')}`);
2777
+ console.log(` ID: ${chalk_1.default.gray(t.id)}`);
2778
+ console.log(` Created: ${new Date(t.createdAt).toLocaleDateString()}`);
2779
+ console.log(` Expires: ${expires}`);
2780
+ console.log(` Last used: ${lastUsed}`);
2781
+ console.log(` Scopes: ${(t.scopes || []).join(', ') || 'none'}`);
2782
+ console.log();
2783
+ }
2784
+ }
2785
+ async function runPatRevoke(id) {
2786
+ const session = resolveSession();
2787
+ const { domain, access_token: token } = session;
2788
+ const data = await graphqlRequest(domain, token, `
2789
+ mutation ($input: RevokePersonalAccessTokenInput!) {
2790
+ revokePersonalAccessToken(input: $input) {
2791
+ personalAccessToken { id name revokedAt }
2792
+ }
2793
+ }
2794
+ `, { input: { id } });
2795
+ const revoked = data?.revokePersonalAccessToken?.personalAccessToken;
2796
+ if (revoked) {
2797
+ console.log(chalk_1.default.green(`PAT token revoked: ${revoked.name || revoked.id}`));
2798
+ }
2799
+ else {
2800
+ console.log(chalk_1.default.green('PAT token revoked.'));
2801
+ }
2802
+ }
2803
+ async function runPatSetup() {
2804
+ const patToken = process.env.CXTMS_AUTH;
2805
+ const server = process.env.CXTMS_SERVER || resolveDomainFromAppYaml();
2806
+ console.log(chalk_1.default.bold('PAT Token Status:\n'));
2807
+ if (patToken) {
2808
+ const masked = patToken.slice(0, 8) + '...' + patToken.slice(-4);
2809
+ console.log(chalk_1.default.green(` CXTMS_AUTH is set: ${masked}`));
2810
+ }
2811
+ else {
2812
+ console.log(chalk_1.default.yellow(' CXTMS_AUTH is not set'));
2813
+ }
2814
+ if (server) {
2815
+ console.log(chalk_1.default.green(` Server: ${server}`));
2816
+ }
2817
+ else {
2818
+ console.log(chalk_1.default.yellow(' Server: not configured (add `server` to app.yaml or set CXTMS_SERVER)'));
2819
+ }
2820
+ console.log();
2821
+ if (patToken && server) {
2822
+ console.log(chalk_1.default.green('PAT authentication is active. OAuth login will be skipped.'));
2823
+ }
2824
+ else {
2825
+ console.log(chalk_1.default.bold('To set up PAT authentication:'));
2826
+ console.log();
2827
+ console.log(chalk_1.default.white(' 1. Create a token:'));
2828
+ console.log(chalk_1.default.cyan(' cxtms pat create "my-token-name"'));
2829
+ console.log();
2830
+ console.log(chalk_1.default.white(' 2. Add to your project .env file:'));
2831
+ console.log(chalk_1.default.cyan(' CXTMS_AUTH=pat_xxxxx'));
2832
+ console.log(chalk_1.default.cyan(' CXTMS_SERVER=https://your-server.com'));
2833
+ console.log();
2834
+ console.log(chalk_1.default.gray(' Or set `server` in app.yaml instead of CXTMS_SERVER.'));
2835
+ }
2836
+ }
2837
+ async function runPublish(featureDir, orgOverride) {
2838
+ const session = resolveSession();
2839
+ const domain = session.domain;
2840
+ const token = session.access_token;
2841
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2842
+ // Read app.yaml
2843
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
2844
+ if (!fs.existsSync(appYamlPath)) {
2845
+ console.error(chalk_1.default.red('Error: app.yaml not found in current directory'));
2846
+ process.exit(2);
2847
+ }
2848
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
2849
+ const appManifestId = appYaml?.id;
2850
+ const appName = appYaml?.name || 'unknown';
2851
+ console.log(chalk_1.default.bold.cyan('\n Publish\n'));
2852
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2853
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2854
+ console.log(chalk_1.default.gray(` App: ${appName}`));
2855
+ if (featureDir) {
2856
+ console.log(chalk_1.default.gray(` Feature: ${featureDir}`));
2857
+ }
2858
+ console.log('');
2859
+ // Step 1: Create or update app manifest
2860
+ if (appManifestId) {
2861
+ console.log(chalk_1.default.gray(' Publishing app manifest...'));
2862
+ try {
2863
+ const checkData = await graphqlRequest(domain, token, `
2864
+ query ($organizationId: Int!, $appManifestId: UUID!) {
2865
+ appManifest(organizationId: $organizationId, appManifestId: $appManifestId) { appManifestId }
2866
+ }
2867
+ `, { organizationId: orgId, appManifestId });
2868
+ if (checkData?.appManifest) {
2869
+ await graphqlRequest(domain, token, `
2870
+ mutation ($input: UpdateAppManifestInput!) {
2871
+ updateAppManifest(input: $input) { appManifest { appManifestId name } }
2872
+ }
2873
+ `, { input: { organizationId: orgId, appManifestId, values: { name: appName, description: appYaml?.description || '' } } });
2874
+ console.log(chalk_1.default.green(' ✓ App manifest updated'));
2875
+ }
2876
+ else {
2877
+ await graphqlRequest(domain, token, `
2878
+ mutation ($input: CreateAppManifestInput!) {
2879
+ createAppManifest(input: $input) { appManifest { appManifestId name } }
2880
+ }
2881
+ `, { input: { organizationId: orgId, values: { appManifestId, name: appName, description: appYaml?.description || '' } } });
2882
+ console.log(chalk_1.default.green(' ✓ App manifest created'));
2883
+ }
2884
+ }
2885
+ catch (e) {
2886
+ console.log(chalk_1.default.red(` ✗ App manifest failed: ${e.message}`));
2887
+ }
2888
+ }
2889
+ // Step 2: Discover files
2890
+ const baseDir = featureDir ? path.join(process.cwd(), 'features', featureDir) : process.cwd();
2891
+ if (featureDir && !fs.existsSync(baseDir)) {
2892
+ console.error(chalk_1.default.red(`Error: Feature directory not found: features/${featureDir}`));
2893
+ process.exit(2);
2894
+ }
2895
+ const workflowDirs = [path.join(baseDir, 'workflows')];
2896
+ const moduleDirs = [path.join(baseDir, 'modules')];
2897
+ // Collect YAML files
2898
+ const workflowFiles = [];
2899
+ const moduleFiles = [];
2900
+ for (const dir of workflowDirs) {
2901
+ if (fs.existsSync(dir)) {
2902
+ for (const f of fs.readdirSync(dir)) {
2903
+ if (f.endsWith('.yaml') || f.endsWith('.yml')) {
2904
+ workflowFiles.push(path.join(dir, f));
2905
+ }
2906
+ }
2907
+ }
2908
+ }
2909
+ for (const dir of moduleDirs) {
2910
+ if (fs.existsSync(dir)) {
2911
+ for (const f of fs.readdirSync(dir)) {
2912
+ if (f.endsWith('.yaml') || f.endsWith('.yml')) {
2913
+ moduleFiles.push(path.join(dir, f));
2914
+ }
2915
+ }
2916
+ }
2917
+ }
2918
+ console.log(chalk_1.default.gray(`\n Found ${workflowFiles.length} workflow(s), ${moduleFiles.length} module(s)\n`));
2919
+ let succeeded = 0;
2920
+ let failed = 0;
2921
+ // Step 3: Deploy workflows
2922
+ for (const file of workflowFiles) {
2923
+ const relPath = path.relative(process.cwd(), file);
2924
+ const result = await pushWorkflowQuiet(domain, token, orgId, file, appManifestId);
2925
+ if (result.ok) {
2926
+ console.log(chalk_1.default.green(` ✓ ${relPath}`));
2927
+ succeeded++;
2928
+ }
2929
+ else {
2930
+ console.log(chalk_1.default.red(` ✗ ${relPath}: ${result.error}`));
2931
+ failed++;
2932
+ }
2933
+ }
2934
+ // Step 4: Deploy modules
2935
+ for (const file of moduleFiles) {
2936
+ const relPath = path.relative(process.cwd(), file);
2937
+ const result = await pushModuleQuiet(domain, token, orgId, file, appManifestId);
2938
+ if (result.ok) {
2939
+ console.log(chalk_1.default.green(` ✓ ${relPath}`));
2940
+ succeeded++;
2941
+ }
2942
+ else {
2943
+ console.log(chalk_1.default.red(` ✗ ${relPath}: ${result.error}`));
2944
+ failed++;
2945
+ }
2946
+ }
2947
+ // Summary
2948
+ console.log('');
2949
+ if (failed === 0) {
2950
+ console.log(chalk_1.default.green(` ✓ Published ${succeeded} file(s) successfully\n`));
2951
+ }
2952
+ else {
2953
+ console.log(chalk_1.default.yellow(` Published ${succeeded} file(s), ${failed} failed\n`));
2954
+ }
2955
+ }
2956
+ // ============================================================================
2957
+ // App Manifest Commands (install from git, publish to git, list)
2958
+ // ============================================================================
2959
+ function readAppYaml() {
2960
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
2961
+ if (!fs.existsSync(appYamlPath)) {
2962
+ console.error(chalk_1.default.red('Error: app.yaml not found in current directory'));
2963
+ process.exit(2);
2964
+ }
2965
+ return yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
2966
+ }
2967
+ async function runAppInstall(orgOverride, branch, force, skipChanged) {
2968
+ const session = resolveSession();
2969
+ const domain = session.domain;
2970
+ const token = session.access_token;
2971
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2972
+ const appYaml = readAppYaml();
2973
+ const repository = appYaml.repository;
2974
+ if (!repository) {
2975
+ console.error(chalk_1.default.red('Error: app.yaml must have a `repository` field'));
2976
+ process.exit(2);
2977
+ }
2978
+ const repositoryBranch = branch || appYaml.branch || 'main';
2979
+ console.log(chalk_1.default.bold.cyan('\n App Install\n'));
2980
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2981
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2982
+ console.log(chalk_1.default.gray(` Repository: ${repository}`));
2983
+ console.log(chalk_1.default.gray(` Branch: ${repositoryBranch}`));
2984
+ if (force)
2985
+ console.log(chalk_1.default.gray(` Force: yes`));
2986
+ if (skipChanged)
2987
+ console.log(chalk_1.default.gray(` Skip changed: yes`));
2988
+ console.log('');
2989
+ try {
2990
+ const data = await graphqlRequest(domain, token, `
2991
+ mutation ($input: InstallAppManifestInput!) {
2992
+ installAppManifest(input: $input) {
2993
+ appManifest {
2994
+ appManifestId
2995
+ name
2996
+ currentVersion
2997
+ isEnabled
2998
+ hasUnpublishedChanges
2999
+ isUpdateAvailable
3000
+ }
3001
+ }
3002
+ }
3003
+ `, {
3004
+ input: {
3005
+ organizationId: orgId,
3006
+ values: {
3007
+ repository,
3008
+ repositoryBranch,
3009
+ force: force || false,
3010
+ skipModulesWithChanges: skipChanged || false,
3011
+ }
3012
+ }
3013
+ });
3014
+ const manifest = data?.installAppManifest?.appManifest;
3015
+ if (manifest) {
3016
+ console.log(chalk_1.default.green(` ✓ Installed ${manifest.name} v${manifest.currentVersion}`));
3017
+ if (manifest.hasUnpublishedChanges) {
3018
+ console.log(chalk_1.default.yellow(` Has unpublished changes`));
3019
+ }
3020
+ }
3021
+ else {
3022
+ console.log(chalk_1.default.green(' ✓ Install completed'));
3023
+ }
3024
+ }
3025
+ catch (e) {
3026
+ console.error(chalk_1.default.red(` ✗ Install failed: ${e.message}`));
3027
+ process.exit(1);
3028
+ }
3029
+ console.log('');
3030
+ }
3031
+ async function runAppPublish(orgOverride, message, branch, force, targetFiles) {
3032
+ const session = resolveSession();
3033
+ const domain = session.domain;
3034
+ const token = session.access_token;
3035
+ const orgId = await resolveOrgId(domain, token, orgOverride);
3036
+ if (!message) {
3037
+ console.error(chalk_1.default.red('Error: --message (-m) is required for app release'));
3038
+ console.error(chalk_1.default.gray('Describe what changed, similar to a git commit message.'));
3039
+ console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} app release -m "Add new shipping module"`));
3040
+ process.exit(2);
3041
+ }
3042
+ const appYaml = readAppYaml();
3043
+ const appManifestId = appYaml.id;
3044
+ if (!appManifestId) {
3045
+ console.error(chalk_1.default.red('Error: app.yaml must have an `id` field'));
3046
+ process.exit(2);
3047
+ }
3048
+ console.log(chalk_1.default.bold.cyan('\n App Release\n'));
3049
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
3050
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
3051
+ console.log(chalk_1.default.gray(` App: ${appYaml.name || appManifestId}`));
3052
+ if (message)
3053
+ console.log(chalk_1.default.gray(` Message: ${message}`));
3054
+ if (branch)
3055
+ console.log(chalk_1.default.gray(` Branch: ${branch}`));
3056
+ if (force)
3057
+ console.log(chalk_1.default.gray(` Force: yes`));
3058
+ // Extract workflow/module IDs from target files
3059
+ const workflowIds = [];
3060
+ const moduleIds = [];
3061
+ if (targetFiles && targetFiles.length > 0) {
3062
+ for (const file of targetFiles) {
3063
+ if (!fs.existsSync(file)) {
3064
+ console.error(chalk_1.default.red(` Error: File not found: ${file}`));
3065
+ process.exit(2);
3066
+ }
3067
+ const parsed = yaml_1.default.parse(fs.readFileSync(file, 'utf-8'));
3068
+ if (parsed?.workflow?.workflowId) {
3069
+ workflowIds.push(parsed.workflow.workflowId);
3070
+ console.log(chalk_1.default.gray(` Workflow: ${parsed.workflow.name || parsed.workflow.workflowId}`));
3071
+ }
3072
+ else if (parsed?.module?.appModuleId) {
3073
+ moduleIds.push(parsed.module.appModuleId);
3074
+ console.log(chalk_1.default.gray(` Module: ${parsed.module.name || parsed.module.appModuleId}`));
3075
+ }
3076
+ else {
3077
+ console.error(chalk_1.default.red(` Error: Cannot identify file type: ${file}`));
3078
+ process.exit(2);
3079
+ }
3080
+ }
3081
+ }
3082
+ console.log('');
3083
+ try {
3084
+ const publishValues = {
3085
+ message: message || undefined,
3086
+ branch: branch || undefined,
3087
+ force: force || false,
3088
+ };
3089
+ if (workflowIds.length > 0)
3090
+ publishValues.workflowIds = workflowIds;
3091
+ if (moduleIds.length > 0)
3092
+ publishValues.moduleIds = moduleIds;
3093
+ const data = await graphqlRequest(domain, token, `
3094
+ mutation ($input: PublishAppManifestInput!) {
3095
+ publishAppManifest(input: $input) {
3096
+ appManifest {
3097
+ appManifestId
3098
+ name
3099
+ currentVersion
3100
+ hasUnpublishedChanges
3101
+ }
3102
+ }
3103
+ }
3104
+ `, {
3105
+ input: {
3106
+ organizationId: orgId,
3107
+ appManifestId,
3108
+ values: publishValues,
3109
+ }
3110
+ });
3111
+ const manifest = data?.publishAppManifest?.appManifest;
3112
+ if (manifest) {
3113
+ console.log(chalk_1.default.green(` ✓ Published ${manifest.name} v${manifest.currentVersion}`));
3114
+ }
3115
+ else {
3116
+ console.log(chalk_1.default.green(' ✓ Publish completed'));
3117
+ }
3118
+ }
3119
+ catch (e) {
3120
+ console.error(chalk_1.default.red(` ✗ Publish failed: ${e.message}`));
3121
+ process.exit(1);
3122
+ }
3123
+ console.log('');
3124
+ }
3125
+ async function runAppList(orgOverride) {
3126
+ const session = resolveSession();
3127
+ const domain = session.domain;
3128
+ const token = session.access_token;
3129
+ const orgId = await resolveOrgId(domain, token, orgOverride);
3130
+ console.log(chalk_1.default.bold.cyan('\n App Manifests\n'));
3131
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
3132
+ console.log(chalk_1.default.gray(` Org: ${orgId}\n`));
3133
+ try {
3134
+ const data = await graphqlRequest(domain, token, `
3135
+ query ($organizationId: Int!) {
3136
+ appManifests(organizationId: $organizationId) {
3137
+ items {
3138
+ appManifestId
3139
+ name
3140
+ currentVersion
3141
+ isEnabled
3142
+ hasUnpublishedChanges
3143
+ isUpdateAvailable
3144
+ repository
3145
+ repositoryBranch
3146
+ }
3147
+ }
3148
+ }
3149
+ `, { organizationId: orgId });
3150
+ const items = data?.appManifests?.items || [];
3151
+ if (items.length === 0) {
3152
+ console.log(chalk_1.default.gray(' No app manifests installed\n'));
3153
+ return;
3154
+ }
3155
+ for (const app of items) {
3156
+ const flags = [];
3157
+ if (!app.isEnabled)
3158
+ flags.push(chalk_1.default.red('disabled'));
3159
+ if (app.hasUnpublishedChanges)
3160
+ flags.push(chalk_1.default.yellow('unpublished'));
3161
+ if (app.isUpdateAvailable)
3162
+ flags.push(chalk_1.default.cyan('update available'));
3163
+ const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
3164
+ console.log(` ${chalk_1.default.bold(app.name)} ${chalk_1.default.gray(`v${app.currentVersion}`)}${flagStr}`);
3165
+ console.log(chalk_1.default.gray(` ID: ${app.appManifestId}`));
3166
+ if (app.repository) {
3167
+ console.log(chalk_1.default.gray(` Repo: ${app.repository} (${app.repositoryBranch || 'main'})`));
3168
+ }
3169
+ }
3170
+ console.log('');
3171
+ }
3172
+ catch (e) {
3173
+ console.error(chalk_1.default.red(` ✗ Failed to list apps: ${e.message}`));
3174
+ process.exit(1);
3175
+ }
3176
+ }
3177
+ // ============================================================================
3178
+ // Query Command
3179
+ // ============================================================================
3180
+ async function runQuery(queryArg, variables) {
3181
+ if (!queryArg) {
3182
+ console.error(chalk_1.default.red('Error: query argument required (inline GraphQL string or .graphql/.gql file path)'));
3183
+ process.exit(2);
3184
+ }
3185
+ // Resolve query: file path or inline string
3186
+ let query;
3187
+ if (queryArg.endsWith('.graphql') || queryArg.endsWith('.gql')) {
3188
+ if (!fs.existsSync(queryArg)) {
3189
+ console.error(chalk_1.default.red(`Error: file not found: ${queryArg}`));
3190
+ process.exit(2);
3191
+ }
3192
+ query = fs.readFileSync(queryArg, 'utf-8');
3193
+ }
3194
+ else {
3195
+ query = queryArg;
3196
+ }
3197
+ // Parse variables if provided
3198
+ let vars = {};
3199
+ if (variables) {
3200
+ try {
3201
+ vars = JSON.parse(variables);
3202
+ }
3203
+ catch {
3204
+ console.error(chalk_1.default.red('Error: --vars must be valid JSON'));
3205
+ process.exit(2);
3206
+ }
3207
+ }
3208
+ const session = resolveSession();
3209
+ const data = await graphqlRequest(session.domain, session.access_token, query, vars);
3210
+ console.log(JSON.stringify(data, null, 2));
3211
+ }
3212
+ // ============================================================================
3213
+ // GQL Schema Exploration Command
3214
+ // ============================================================================
3215
+ async function runGql(sub, filter) {
3216
+ if (!sub) {
3217
+ console.error(chalk_1.default.red('Error: subcommand required'));
3218
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql <queries|mutations|types|type> [name] [--filter <text>]`));
3219
+ process.exit(2);
3220
+ }
3221
+ const session = resolveSession();
3222
+ if (sub === 'type') {
3223
+ if (!filter) {
3224
+ console.error(chalk_1.default.red('Error: type name required'));
3225
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql type <TypeName>`));
3226
+ process.exit(2);
3227
+ }
3228
+ await runGqlType(session, filter);
3229
+ }
3230
+ else if (sub === 'queries') {
3231
+ await runGqlRootFields(session, 'queryType', filter);
3232
+ }
3233
+ else if (sub === 'mutations') {
3234
+ await runGqlRootFields(session, 'mutationType', filter);
3235
+ }
3236
+ else if (sub === 'types') {
3237
+ await runGqlTypes(session, filter);
3238
+ }
3239
+ else {
3240
+ console.error(chalk_1.default.red(`Unknown gql subcommand: ${sub}`));
3241
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql <queries|mutations|types|type> [--filter <text>]`));
3242
+ process.exit(2);
3243
+ }
3244
+ }
3245
+ function formatGqlType(t) {
3246
+ if (!t)
3247
+ return 'unknown';
3248
+ if (t.kind === 'NON_NULL')
3249
+ return `${formatGqlType(t.ofType)}!`;
3250
+ if (t.kind === 'LIST')
3251
+ return `[${formatGqlType(t.ofType)}]`;
3252
+ return t.name || 'unknown';
3253
+ }
3254
+ async function runGqlType(session, typeName) {
3255
+ const query = `{
3256
+ __type(name: "${typeName}") {
3257
+ name kind description
3258
+ fields { name description type { name kind ofType { name kind ofType { name kind ofType { name kind } } } } args { name type { name kind ofType { name kind ofType { name kind } } } defaultValue } }
3259
+ inputFields { name type { name kind ofType { name kind ofType { name kind } } } defaultValue }
3260
+ enumValues { name description }
3261
+ }
3262
+ }`;
3263
+ const data = await graphqlRequest(session.domain, session.access_token, query, {});
3264
+ const type = data.__type;
3265
+ if (!type) {
3266
+ console.error(chalk_1.default.red(`Type "${typeName}" not found`));
3267
+ process.exit(1);
3268
+ }
3269
+ console.log(chalk_1.default.bold.cyan(`${type.name}`) + chalk_1.default.gray(` (${type.kind})`));
3270
+ if (type.description)
3271
+ console.log(chalk_1.default.gray(type.description));
3272
+ console.log('');
3273
+ if (type.fields && type.fields.length > 0) {
3274
+ console.log(chalk_1.default.bold.yellow('Fields:'));
3275
+ for (const f of type.fields) {
3276
+ const typeStr = formatGqlType(f.type);
3277
+ let line = ` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(typeStr)}`;
3278
+ if (f.args && f.args.length > 0) {
3279
+ const argsStr = f.args.map((a) => {
3280
+ const argType = formatGqlType(a.type);
3281
+ return a.defaultValue ? `${a.name}: ${argType} = ${a.defaultValue}` : `${a.name}: ${argType}`;
3282
+ }).join(', ');
3283
+ line += chalk_1.default.gray(` (${argsStr})`);
3284
+ }
3285
+ if (f.description)
3286
+ line += chalk_1.default.gray(` — ${f.description}`);
3287
+ console.log(line);
3288
+ }
3289
+ }
3290
+ if (type.inputFields && type.inputFields.length > 0) {
3291
+ console.log(chalk_1.default.bold.yellow('Input Fields:'));
3292
+ for (const f of type.inputFields) {
3293
+ const typeStr = formatGqlType(f.type);
3294
+ let line = ` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(typeStr)}`;
3295
+ if (f.defaultValue)
3296
+ line += chalk_1.default.gray(` = ${f.defaultValue}`);
3297
+ console.log(line);
3298
+ }
3299
+ }
3300
+ if (type.enumValues && type.enumValues.length > 0) {
3301
+ console.log(chalk_1.default.bold.yellow('Enum Values:'));
3302
+ for (const v of type.enumValues) {
3303
+ let line = ` ${chalk_1.default.green(v.name)}`;
3304
+ if (v.description)
3305
+ line += chalk_1.default.gray(` — ${v.description}`);
3306
+ console.log(line);
3307
+ }
3308
+ }
3309
+ }
3310
+ async function runGqlRootFields(session, rootType, filter) {
3311
+ const query = `{
3312
+ __schema {
3313
+ ${rootType} {
3314
+ fields { name description args { name type { name kind ofType { name kind ofType { name kind } } } defaultValue } type { name kind ofType { name kind ofType { name kind } } } }
3315
+ }
3316
+ }
3317
+ }`;
3318
+ const data = await graphqlRequest(session.domain, session.access_token, query, {});
3319
+ const fields = data.__schema?.[rootType]?.fields || [];
3320
+ const filtered = filter
3321
+ ? fields.filter((f) => f.name.toLowerCase().includes(filter.toLowerCase()))
3322
+ : fields;
3323
+ const label = rootType === 'queryType' ? 'Queries' : 'Mutations';
3324
+ console.log(chalk_1.default.bold.yellow(`${label}${filter ? ` (filter: "${filter}")` : ''}:`));
3325
+ console.log('');
3326
+ for (const f of filtered) {
3327
+ const returnType = formatGqlType(f.type);
3328
+ console.log(` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(returnType)}`);
3329
+ if (f.description)
3330
+ console.log(` ${chalk_1.default.gray(f.description)}`);
3331
+ if (f.args && f.args.length > 0) {
3332
+ for (const a of f.args) {
3333
+ const argType = formatGqlType(a.type);
3334
+ const def = a.defaultValue ? chalk_1.default.gray(` = ${a.defaultValue}`) : '';
3335
+ console.log(` ${chalk_1.default.white(a.name)}: ${chalk_1.default.cyan(argType)}${def}`);
3336
+ }
3337
+ }
3338
+ console.log('');
3339
+ }
3340
+ console.log(chalk_1.default.gray(`${filtered.length} ${label.toLowerCase()} found`));
3341
+ }
3342
+ async function runGqlTypes(session, filter) {
3343
+ const query = `{
3344
+ __schema {
3345
+ types { name kind description }
3346
+ }
3347
+ }`;
3348
+ const data = await graphqlRequest(session.domain, session.access_token, query, {});
3349
+ const types = (data.__schema?.types || [])
3350
+ .filter((t) => !t.name.startsWith('__'))
3351
+ .filter((t) => !filter || t.name.toLowerCase().includes(filter.toLowerCase()));
3352
+ const grouped = {};
3353
+ for (const t of types) {
3354
+ const kind = t.kind || 'OTHER';
3355
+ if (!grouped[kind])
3356
+ grouped[kind] = [];
3357
+ grouped[kind].push(t);
3358
+ }
3359
+ const kindOrder = ['OBJECT', 'INPUT_OBJECT', 'ENUM', 'INTERFACE', 'UNION', 'SCALAR'];
3360
+ for (const kind of kindOrder) {
3361
+ const items = grouped[kind];
3362
+ if (!items || items.length === 0)
3363
+ continue;
3364
+ console.log(chalk_1.default.bold.yellow(`${kind} (${items.length}):`));
3365
+ for (const t of items.sort((a, b) => a.name.localeCompare(b.name))) {
3366
+ let line = ` ${chalk_1.default.green(t.name)}`;
3367
+ if (t.description)
3368
+ line += chalk_1.default.gray(` — ${t.description}`);
3369
+ console.log(line);
3370
+ }
3371
+ console.log('');
3372
+ }
3373
+ console.log(chalk_1.default.gray(`${types.length} types found${filter ? ` matching "${filter}"` : ''}`));
3374
+ }
3375
+ // ============================================================================
3376
+ // Extract Command
3377
+ // ============================================================================
3378
+ function runExtract(sourceFile, componentName, targetFile, copy) {
3379
+ // Validate args
3380
+ if (!sourceFile || !componentName || !targetFile) {
3381
+ console.error(chalk_1.default.red('Error: Missing required arguments'));
3382
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file> [--copy]`));
3383
+ process.exit(2);
3384
+ }
3385
+ // Check source exists
3386
+ if (!fs.existsSync(sourceFile)) {
3387
+ console.error(chalk_1.default.red(`Error: Source file not found: ${sourceFile}`));
3388
+ process.exit(2);
3389
+ }
3390
+ // Read and parse source (Document API preserves comments)
3391
+ const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
3392
+ const srcDoc = yaml_1.default.parseDocument(sourceContent);
3393
+ const sourceJS = srcDoc.toJS();
3394
+ if (!sourceJS || !Array.isArray(sourceJS.components)) {
3395
+ console.error(chalk_1.default.red(`Error: Source file is not a valid module (missing components array): ${sourceFile}`));
3396
+ process.exit(2);
3397
+ }
3398
+ // Get the AST components sequence
3399
+ const srcComponents = srcDoc.get('components', true);
3400
+ if (!(0, yaml_1.isSeq)(srcComponents)) {
3401
+ console.error(chalk_1.default.red(`Error: Source components is not a sequence: ${sourceFile}`));
3402
+ process.exit(2);
3403
+ }
3404
+ // Find component by exact name match
3405
+ const compIndex = srcComponents.items.findIndex((item) => {
3406
+ return (0, yaml_1.isMap)(item) && item.get('name') === componentName;
3407
+ });
3408
+ if (compIndex === -1) {
3409
+ const available = sourceJS.components.map((c) => c.name).filter(Boolean);
3410
+ console.error(chalk_1.default.red(`Error: Component not found: ${componentName}`));
3411
+ if (available.length > 0) {
3412
+ console.error(chalk_1.default.gray('Available components:'));
3413
+ for (const name of available) {
3414
+ console.error(chalk_1.default.gray(` - ${name}`));
3415
+ }
3416
+ }
3417
+ process.exit(2);
3418
+ }
3419
+ // Get the component AST node (clone for copy, take for move)
3420
+ const componentNode = copy
3421
+ ? srcDoc.createNode(sourceJS.components[compIndex])
3422
+ : srcComponents.items[compIndex];
3423
+ // Capture comment: if this is the first item, the comment lives on the parent seq
3424
+ let componentComment;
3425
+ if (compIndex === 0 && srcComponents.commentBefore) {
3426
+ componentComment = srcComponents.commentBefore;
3427
+ if (!copy) {
3428
+ // Transfer the comment away from the source seq (it belongs to the extracted component)
3429
+ srcComponents.commentBefore = undefined;
3430
+ }
3431
+ }
3432
+ else {
3433
+ componentComment = componentNode.commentBefore;
3434
+ }
3435
+ // Find matching routes (by index in AST)
3436
+ const srcRoutes = srcDoc.get('routes', true);
3437
+ const matchedRouteIndices = [];
3438
+ if ((0, yaml_1.isSeq)(srcRoutes)) {
3439
+ srcRoutes.items.forEach((item, idx) => {
3440
+ if ((0, yaml_1.isMap)(item) && item.get('component') === componentName) {
3441
+ matchedRouteIndices.push(idx);
3442
+ }
3443
+ });
3444
+ }
3445
+ // Collect route AST nodes (clone for copy, reference for move)
3446
+ const routeNodes = matchedRouteIndices.map(idx => {
3447
+ if (copy) {
3448
+ return srcDoc.createNode(sourceJS.routes[idx]);
3449
+ }
3450
+ return srcRoutes.items[idx];
3451
+ });
3452
+ // Load or create target document
3453
+ let tgtDoc;
3454
+ let targetCreated = false;
3455
+ if (fs.existsSync(targetFile)) {
3456
+ const targetContent = fs.readFileSync(targetFile, 'utf-8');
3457
+ tgtDoc = yaml_1.default.parseDocument(targetContent);
3458
+ const targetJS = tgtDoc.toJS();
3459
+ if (!targetJS || !Array.isArray(targetJS.components)) {
3460
+ console.error(chalk_1.default.red(`Error: Target file is not a valid module (missing components array): ${targetFile}`));
3461
+ process.exit(2);
3462
+ }
3463
+ // Check for duplicate component name
3464
+ const duplicate = targetJS.components.find((c) => c.name === componentName);
3465
+ if (duplicate) {
3466
+ console.error(chalk_1.default.red(`Error: Target already contains a component named "${componentName}"`));
3467
+ process.exit(2);
3468
+ }
3469
+ }
3470
+ else {
3471
+ // Create new module scaffold
3472
+ const baseName = path.basename(targetFile, path.extname(targetFile));
3473
+ const moduleName = baseName
3474
+ .split('-')
3475
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
3476
+ .join('');
3477
+ const sourceModule = typeof sourceJS.module === 'object' ? sourceJS.module : null;
3478
+ const displayName = moduleName.replace(/([a-z])([A-Z])/g, '$1 $2');
3479
+ const moduleObj = {
3480
+ name: moduleName,
3481
+ appModuleId: generateUUID(),
3482
+ displayName: { 'en-US': displayName },
3483
+ description: { 'en-US': `${displayName} module` },
3484
+ application: 'System',
3485
+ };
3486
+ // In copy mode, set priority higher than source
3487
+ if (copy) {
3488
+ const sourcePriority = sourceModule?.priority;
3489
+ moduleObj.priority = (0, extractUtils_1.computeExtractPriority)(sourcePriority);
3490
+ }
3491
+ // Parse from string so the document has proper AST context for comment preservation
3492
+ const scaffoldStr = yaml_1.default.stringify({
3493
+ module: moduleObj,
3494
+ entities: [],
3495
+ permissions: [],
3496
+ components: [],
3497
+ routes: []
3498
+ }, { indent: 2, lineWidth: 0, singleQuote: false });
3499
+ tgtDoc = yaml_1.default.parseDocument(scaffoldStr);
3500
+ targetCreated = true;
3501
+ }
3502
+ // Add component to target (ensure block style so comments are preserved)
3503
+ const tgtComponents = tgtDoc.get('components', true);
3504
+ if ((0, yaml_1.isSeq)(tgtComponents)) {
3505
+ tgtComponents.flow = false;
3506
+ // Apply the captured comment: if it's the first item in target, set on seq; otherwise on node
3507
+ if (componentComment) {
3508
+ if (tgtComponents.items.length === 0) {
3509
+ tgtComponents.commentBefore = componentComment;
3510
+ }
3511
+ else {
3512
+ componentNode.commentBefore = componentComment;
3513
+ }
3514
+ }
3515
+ tgtComponents.items.push(componentNode);
3516
+ }
3517
+ else {
3518
+ tgtDoc.addIn(['components'], componentNode);
3519
+ }
3520
+ // In move mode, remove component from source
3521
+ if (!copy) {
3522
+ srcComponents.items.splice(compIndex, 1);
3523
+ }
3524
+ // Add routes to target
3525
+ if (routeNodes.length > 0) {
3526
+ let tgtRoutes = tgtDoc.get('routes', true);
3527
+ if (!(0, yaml_1.isSeq)(tgtRoutes)) {
3528
+ tgtDoc.set('routes', tgtDoc.createNode([]));
3529
+ tgtRoutes = tgtDoc.get('routes', true);
3530
+ }
3531
+ tgtRoutes.flow = false;
3532
+ for (const routeNode of routeNodes) {
3533
+ tgtRoutes.items.push(routeNode);
3534
+ }
3535
+ // In move mode, remove routes from source (reverse order to preserve indices)
3536
+ if (!copy && (0, yaml_1.isSeq)(srcRoutes)) {
3537
+ for (let i = matchedRouteIndices.length - 1; i >= 0; i--) {
3538
+ srcRoutes.items.splice(matchedRouteIndices[i], 1);
3539
+ }
3540
+ }
3541
+ }
3542
+ // Ensure target directory exists
3543
+ const targetDir = path.dirname(targetFile);
3544
+ if (!fs.existsSync(targetDir)) {
3545
+ fs.mkdirSync(targetDir, { recursive: true });
3546
+ }
3547
+ // Write files (toString preserves comments)
3548
+ const toStringOpts = { indent: 2, lineWidth: 0, singleQuote: false };
3549
+ if (!copy) {
3550
+ fs.writeFileSync(sourceFile, srcDoc.toString(toStringOpts), 'utf-8');
3551
+ }
3552
+ fs.writeFileSync(targetFile, tgtDoc.toString(toStringOpts), 'utf-8');
3553
+ // Print summary
3554
+ const action = copy ? 'Copied' : 'Extracted';
3555
+ console.log(chalk_1.default.green(`\n✓ ${action} component: ${chalk_1.default.bold(componentName)}`));
3556
+ console.log(chalk_1.default.gray(` Routes ${copy ? 'copied' : 'moved'}: ${matchedRouteIndices.length}`));
3557
+ if (!copy) {
3558
+ console.log(chalk_1.default.gray(` Source: ${sourceFile} (updated)`));
3559
+ }
3560
+ else {
3561
+ console.log(chalk_1.default.gray(` Source: ${sourceFile} (unchanged)`));
3562
+ }
3563
+ console.log(chalk_1.default.gray(` Target: ${targetFile} (${targetCreated ? 'created' : 'updated'})`));
3564
+ console.log('');
3565
+ }
3566
+ // ============================================================================
3567
+ // Argument Parsing
3568
+ // ============================================================================
3569
+ function parseArgs(args) {
3570
+ const files = [];
3571
+ let command = null;
3572
+ const options = {
3573
+ help: false,
3574
+ version: false,
3575
+ type: 'auto',
3576
+ format: 'pretty',
3577
+ verbose: false,
3578
+ showSchema: null,
3579
+ showExample: null,
3580
+ listSchemas: false,
3581
+ listTasks: false,
3582
+ quiet: false,
3583
+ reportFormat: 'json'
3584
+ };
3585
+ // Check for commands
3586
+ const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude', 'login', 'logout', 'pat', 'appmodule', 'orgs', 'workflow', 'publish', 'query', 'gql', 'app'];
3587
+ if (args.length > 0 && commands.includes(args[0])) {
3588
+ command = args[0];
3589
+ args = args.slice(1);
3590
+ }
3591
+ for (let i = 0; i < args.length; i++) {
3592
+ const arg = args[i];
3593
+ if (arg === '--help' || arg === '-h') {
3594
+ options.help = true;
3595
+ }
3596
+ else if (arg === '--version' || arg === '-v') {
3597
+ options.version = true;
3598
+ }
3599
+ else if (arg === '--schemas' || arg === '-s') {
3600
+ options.schemasPath = args[++i];
3601
+ }
3602
+ else if (arg === '--type' || arg === '-t') {
3603
+ const typeArg = args[++i];
3604
+ if (['module', 'workflow', 'auto'].includes(typeArg)) {
3605
+ options.type = typeArg;
3606
+ }
3607
+ else {
3608
+ console.error(chalk_1.default.red(`Invalid type: ${typeArg}. Use: module, workflow, or auto`));
3609
+ process.exit(2);
3610
+ }
3611
+ }
3612
+ else if (arg === '--format' || arg === '-f') {
3613
+ const formatArg = args[++i];
3614
+ if (['pretty', 'json', 'compact'].includes(formatArg)) {
3615
+ options.format = formatArg;
3616
+ }
3617
+ else {
3618
+ console.error(chalk_1.default.red(`Invalid format: ${formatArg}. Use: pretty, json, or compact`));
3619
+ process.exit(2);
3620
+ }
3621
+ }
3622
+ else if (arg === '--verbose') {
3623
+ options.verbose = true;
3624
+ }
3625
+ else if (arg === '--quiet' || arg === '-q') {
3626
+ options.quiet = true;
3627
+ }
3628
+ else if (arg === '--json') {
3629
+ options.format = 'json';
3630
+ }
3631
+ else if (arg === '--report' || arg === '-r') {
3632
+ options.report = args[++i];
3633
+ }
3634
+ else if (arg === '--report-format') {
3635
+ const reportFormatArg = args[++i];
3636
+ if (['html', 'markdown', 'json'].includes(reportFormatArg)) {
3637
+ options.reportFormat = reportFormatArg;
3638
+ }
3639
+ else {
3640
+ console.error(chalk_1.default.red(`Invalid report format: ${reportFormatArg}. Use: html, markdown, or json`));
3641
+ process.exit(2);
3642
+ }
3643
+ }
3644
+ else if (arg === '--template') {
3645
+ options.template = args[++i];
3646
+ }
3647
+ else if (arg === '--feature') {
3648
+ options.feature = args[++i];
3649
+ }
3650
+ else if (arg === '--options') {
3651
+ options.createOptions = args[++i];
3652
+ }
3653
+ else if (arg === '--tasks') {
3654
+ options.createTasks = args[++i];
3655
+ }
3656
+ else if (arg === '--to') {
3657
+ options.extractTo = args[++i];
3658
+ }
3659
+ else if (arg === '--copy') {
3660
+ options.extractCopy = true;
3661
+ }
3662
+ else if (arg === '--org') {
3663
+ const orgArg = args[++i];
3664
+ const parsed = parseInt(orgArg, 10);
3665
+ if (isNaN(parsed)) {
3666
+ console.error(chalk_1.default.red(`Invalid --org value: ${orgArg}. Must be a number.`));
3667
+ process.exit(2);
3668
+ }
3669
+ options.orgId = parsed;
3670
+ }
3671
+ else if (arg === '--vars') {
3672
+ options.vars = args[++i];
3673
+ }
3674
+ else if (arg === '--from') {
3675
+ options.from = args[++i];
3676
+ }
3677
+ else if (arg === '--to') {
3678
+ options.to = args[++i];
3679
+ }
3680
+ else if (arg === '--output' || arg === '-o') {
3681
+ options.output = args[++i];
3682
+ }
3683
+ else if (arg === '--console') {
3684
+ options.console = true;
3685
+ }
3686
+ else if (arg === '--message' || arg === '-m') {
3687
+ options.message = args[++i];
3688
+ }
3689
+ else if (arg === '--branch' || arg === '-b') {
3690
+ options.branch = args[++i];
3691
+ }
3692
+ else if (arg === '--file') {
3693
+ if (!options.file)
3694
+ options.file = [];
3695
+ options.file.push(args[++i]);
3696
+ }
3697
+ else if (arg === '--filter') {
3698
+ options.filter = args[++i];
3699
+ }
3700
+ else if (arg === '--force') {
3701
+ options.force = true;
3702
+ }
3703
+ else if (arg === '--skip-changed') {
3704
+ options.skipChanged = true;
3705
+ }
3706
+ else if (!arg.startsWith('-')) {
3707
+ files.push(arg);
3708
+ }
3709
+ else {
3710
+ console.error(chalk_1.default.red(`Unknown option: ${arg}`));
3711
+ console.error(`Use ${chalk_1.default.cyan(`${PROGRAM_NAME} --help`)} for usage information`);
3712
+ process.exit(2);
3713
+ }
3714
+ }
3715
+ // Handle schema command
3716
+ if (command === 'schema' && files.length > 0) {
3717
+ options.showSchema = files[0];
3718
+ }
3719
+ // Handle example command
3720
+ if (command === 'example' && files.length > 0) {
3721
+ options.showExample = files[0];
3722
+ }
3723
+ // Handle list command
3724
+ if (command === 'list') {
3725
+ options.listSchemas = true;
3726
+ }
3727
+ // Handle help command
3728
+ if (command === 'help') {
3729
+ options.help = true;
3730
+ }
3731
+ // Handle version command
3732
+ if (command === 'version') {
3733
+ options.version = true;
3734
+ }
3735
+ return { command, files, options };
3736
+ }
3737
+ // ============================================================================
3738
+ // Schema Path Finding
3739
+ // ============================================================================
3740
+ function findSchemasPath() {
3741
+ // Check environment variable
3742
+ if (process.env.CX_SCHEMA_PATH && fs.existsSync(process.env.CX_SCHEMA_PATH)) {
3743
+ return process.env.CX_SCHEMA_PATH;
3744
+ }
3745
+ // Check for .cx-schema in current directory
3746
+ const localSchemas = path.join(process.cwd(), '.cx-schema');
3747
+ if (fs.existsSync(localSchemas)) {
3748
+ return localSchemas;
3749
+ }
3750
+ // Check for schemas in node_modules
3751
+ const nodeModulesSchemas = path.join(process.cwd(), 'node_modules', '@cxtms/cx-schema', 'schemas');
3752
+ if (fs.existsSync(nodeModulesSchemas)) {
3753
+ return nodeModulesSchemas;
3754
+ }
3755
+ // Check in package directory (for development)
3756
+ const packageSchemas = path.join(__dirname, '../schemas');
3757
+ if (fs.existsSync(packageSchemas)) {
3758
+ return packageSchemas;
3759
+ }
3760
+ return undefined;
3761
+ }
3762
+ // ============================================================================
3763
+ // Auto-detection
3764
+ // ============================================================================
3765
+ function detectFileType(filePath) {
3766
+ try {
3767
+ const content = fs.readFileSync(filePath, 'utf-8');
3768
+ const data = yaml_1.default.parse(content);
3769
+ if (data && typeof data === 'object') {
3770
+ if ('workflow' in data) {
3771
+ return 'workflow';
3772
+ }
3773
+ if ('module' in data || 'components' in data) {
3774
+ return 'module';
3775
+ }
3776
+ }
3777
+ // Check file path for hints
3778
+ if (filePath.includes('workflow')) {
3779
+ return 'workflow';
3780
+ }
3781
+ if (filePath.includes('module')) {
3782
+ return 'module';
3783
+ }
3784
+ // Default to module
3785
+ return 'module';
3786
+ }
3787
+ catch {
3788
+ return 'module';
3789
+ }
3790
+ }
3791
+ // ============================================================================
3792
+ // Schema Display
3793
+ // ============================================================================
3794
+ // Cache for dynamically discovered workflow task schema names
3795
+ let _workflowTaskNamesCache = null;
3796
+ function getWorkflowTaskNames(schemasPath) {
3797
+ if (_workflowTaskNamesCache)
3798
+ return _workflowTaskNamesCache;
3799
+ const tasksDir = path.join(schemasPath, 'workflows', 'tasks');
3800
+ _workflowTaskNamesCache = new Set();
3801
+ if (fs.existsSync(tasksDir)) {
3802
+ for (const file of fs.readdirSync(tasksDir)) {
3803
+ if (file.endsWith('.json') && file !== 'all.json') {
3804
+ _workflowTaskNamesCache.add(file.replace('.json', '').toLowerCase().replace(/[^a-z0-9-]/g, ''));
3805
+ }
3806
+ }
3807
+ }
3808
+ // Also include common definitions
3809
+ const commonDir = path.join(schemasPath, 'workflows', 'common');
3810
+ if (fs.existsSync(commonDir)) {
3811
+ for (const file of fs.readdirSync(commonDir)) {
3812
+ if (file.endsWith('.json')) {
3813
+ _workflowTaskNamesCache.add(file.replace('.json', '').toLowerCase().replace(/[^a-z0-9-]/g, ''));
3814
+ }
3815
+ }
3816
+ }
3817
+ return _workflowTaskNamesCache;
3818
+ }
3819
+ function findSchemaFile(schemasPath, name, preferWorkflow = false) {
3820
+ // Normalize name: lowercase, strip non-alphanumeric except hyphens
3821
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '');
3822
+ // Dynamically detect workflow schema names from directory contents
3823
+ const workflowCoreNames = ['workflow', 'activity', 'input', 'output', 'variable', 'trigger', 'schedule'];
3824
+ const workflowTaskNames = getWorkflowTaskNames(schemasPath);
3825
+ const isWorkflowSchema = workflowCoreNames.includes(normalizedName) ||
3826
+ workflowTaskNames.has(normalizedName);
3827
+ // Build search paths using normalized name for consistency
3828
+ const searchPaths = preferWorkflow || isWorkflowSchema
3829
+ ? [
3830
+ // Workflow schemas first for workflow-related names
3831
+ path.join(schemasPath, 'workflows', `${normalizedName}.json`),
3832
+ path.join(schemasPath, 'workflows', 'tasks', `${normalizedName}.json`),
3833
+ path.join(schemasPath, 'workflows', 'common', `${normalizedName}.json`),
3834
+ // Then module schemas
3835
+ path.join(schemasPath, 'components', `${normalizedName}.json`),
3836
+ path.join(schemasPath, 'fields', `${normalizedName}.json`),
3837
+ path.join(schemasPath, 'actions', `${normalizedName}.json`)
3838
+ ]
3839
+ : [
3840
+ // Module schemas first
3841
+ path.join(schemasPath, 'components', `${normalizedName}.json`),
3842
+ path.join(schemasPath, 'fields', `${normalizedName}.json`),
3843
+ path.join(schemasPath, 'actions', `${normalizedName}.json`),
3844
+ // Then workflow schemas
3845
+ path.join(schemasPath, 'workflows', `${normalizedName}.json`),
3846
+ path.join(schemasPath, 'workflows', 'tasks', `${normalizedName}.json`),
3847
+ path.join(schemasPath, 'workflows', 'common', `${normalizedName}.json`)
3848
+ ];
3849
+ for (const schemaPath of searchPaths) {
3850
+ if (fs.existsSync(schemaPath)) {
3851
+ return schemaPath;
3852
+ }
3853
+ }
3854
+ // Also try with the original name (preserving case) for backwards compatibility
3855
+ if (normalizedName !== name) {
3856
+ const caseSensitivePaths = [
3857
+ path.join(schemasPath, 'workflows', 'tasks', `${name}.json`),
3858
+ path.join(schemasPath, 'workflows', `${name}.json`),
3859
+ path.join(schemasPath, 'components', `${name}.json`),
3860
+ path.join(schemasPath, 'fields', `${name}.json`),
3861
+ path.join(schemasPath, 'actions', `${name}.json`)
3862
+ ];
3863
+ for (const schemaPath of caseSensitivePaths) {
3864
+ if (fs.existsSync(schemaPath)) {
3865
+ return schemaPath;
3866
+ }
3867
+ }
3868
+ }
3869
+ // Try fuzzy matching
3870
+ const allSchemas = getAllSchemas(schemasPath);
3871
+ for (const schema of allSchemas) {
3872
+ const schemaBaseName = path.basename(schema, '.json').toLowerCase().replace(/[^a-z0-9]/g, '');
3873
+ if (schemaBaseName === normalizedName || schemaBaseName.includes(normalizedName)) {
3874
+ return schema;
3875
+ }
3876
+ }
3877
+ return null;
3878
+ }
3879
+ function getAllSchemas(schemasPath) {
3880
+ const schemas = [];
3881
+ function scanDir(dir) {
3882
+ if (!fs.existsSync(dir))
3883
+ return;
3884
+ const files = fs.readdirSync(dir);
3885
+ for (const file of files) {
3886
+ const filePath = path.join(dir, file);
3887
+ const stat = fs.statSync(filePath);
3888
+ if (stat.isDirectory()) {
3889
+ scanDir(filePath);
3890
+ }
3891
+ else if (file.endsWith('.json')) {
3892
+ schemas.push(filePath);
3893
+ }
3894
+ }
3895
+ }
3896
+ scanDir(schemasPath);
3897
+ return schemas;
3898
+ }
3899
+ function showSchema(schemasPath, name) {
3900
+ const schemaFile = findSchemaFile(schemasPath, name);
3901
+ if (!schemaFile) {
3902
+ console.error(chalk_1.default.red(`Schema not found: ${name}`));
3903
+ console.error(chalk_1.default.gray(`Use '${PROGRAM_NAME} list' to see available schemas`));
3904
+ process.exit(2);
3905
+ }
3906
+ const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf-8'));
3907
+ const relativePath = path.relative(schemasPath, schemaFile);
3908
+ console.log(chalk_1.default.bold.cyan(`\nSchema: ${relativePath}\n`));
3909
+ console.log(chalk_1.default.gray('─'.repeat(70)));
3910
+ console.log(JSON.stringify(schema, null, 2));
3911
+ console.log(chalk_1.default.gray('─'.repeat(70)));
3912
+ }
3913
+ function showExample(schemasPath, name) {
3914
+ const schemaFile = findSchemaFile(schemasPath, name);
3915
+ if (!schemaFile) {
3916
+ console.error(chalk_1.default.red(`Schema not found: ${name}`));
3917
+ console.error(chalk_1.default.gray(`Use '${PROGRAM_NAME} list' to see available schemas`));
3918
+ process.exit(2);
3919
+ }
3920
+ const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf-8'));
3921
+ const relativePath = path.relative(schemasPath, schemaFile);
3922
+ console.log(chalk_1.default.bold.cyan(`\nExample for: ${relativePath}\n`));
3923
+ console.log(chalk_1.default.gray('─'.repeat(70)));
3924
+ // Generate example from schema
3925
+ const example = generateExampleFromSchema(schema, name);
3926
+ console.log(yaml_1.default.stringify(example, { indent: 2, lineWidth: 100 }));
3927
+ console.log(chalk_1.default.gray('─'.repeat(70)));
3928
+ }
3929
+ function generateExampleFromSchema(schema, name) {
3930
+ // Check for x-example or examples in schema
3931
+ if (schema['x-example']) {
3932
+ return schema['x-example'];
3933
+ }
3934
+ if (schema['x-examples'] && Array.isArray(schema['x-examples'])) {
3935
+ return schema['x-examples'][0];
3936
+ }
3937
+ if (schema.examples && Array.isArray(schema.examples)) {
3938
+ return schema.examples[0];
3939
+ }
3940
+ // Generate basic example from properties
3941
+ const example = {};
3942
+ if (schema.properties) {
3943
+ for (const [key, prop] of Object.entries(schema.properties)) {
3944
+ if (prop['x-example'] !== undefined) {
3945
+ example[key] = prop['x-example'];
3946
+ }
3947
+ else if (prop.const !== undefined) {
3948
+ example[key] = prop.const;
3949
+ }
3950
+ else if (prop.enum && prop.enum.length > 0) {
3951
+ example[key] = prop.enum[0];
3952
+ }
3953
+ else if (prop.type === 'string') {
3954
+ example[key] = prop.description ? `<${key}>` : 'example';
3955
+ }
3956
+ else if (prop.type === 'number' || prop.type === 'integer') {
3957
+ example[key] = 1;
3958
+ }
3959
+ else if (prop.type === 'boolean') {
3960
+ example[key] = true;
3961
+ }
3962
+ else if (prop.type === 'array') {
3963
+ example[key] = [];
3964
+ }
3965
+ else if (prop.type === 'object') {
3966
+ example[key] = {};
3967
+ }
3968
+ }
3969
+ }
3970
+ return example;
3971
+ }
3972
+ function listSchemas(schemasPath, type) {
3973
+ console.log(chalk_1.default.bold.cyan('\n╔═══════════════════════════════════════════════════════════╗'));
3974
+ console.log(chalk_1.default.bold.cyan('║ AVAILABLE SCHEMAS ║'));
3975
+ console.log(chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════╝\n'));
3976
+ if (type === 'auto' || type === 'module') {
3977
+ console.log(chalk_1.default.bold.yellow('MODULE SCHEMAS:'));
3978
+ console.log(chalk_1.default.gray('─'.repeat(50)));
3979
+ // Components
3980
+ const componentsDir = path.join(schemasPath, 'components');
3981
+ if (fs.existsSync(componentsDir)) {
3982
+ console.log(chalk_1.default.bold('\n Components:'));
3983
+ const components = fs.readdirSync(componentsDir)
3984
+ .filter(f => f.endsWith('.json'))
3985
+ .map(f => f.replace('.json', ''));
3986
+ console.log(chalk_1.default.green(' ' + components.join(', ')));
3987
+ }
3988
+ // Fields
3989
+ const fieldsDir = path.join(schemasPath, 'fields');
3990
+ if (fs.existsSync(fieldsDir)) {
3991
+ console.log(chalk_1.default.bold('\n Fields:'));
3992
+ const fields = fs.readdirSync(fieldsDir)
3993
+ .filter(f => f.endsWith('.json'))
3994
+ .map(f => f.replace('.json', ''));
3995
+ console.log(chalk_1.default.green(' ' + fields.join(', ')));
3996
+ }
3997
+ // Actions
3998
+ const actionsDir = path.join(schemasPath, 'actions');
3999
+ if (fs.existsSync(actionsDir)) {
4000
+ console.log(chalk_1.default.bold('\n Actions:'));
4001
+ const actions = fs.readdirSync(actionsDir)
4002
+ .filter(f => f.endsWith('.json'))
4003
+ .map(f => f.replace('.json', ''));
4004
+ console.log(chalk_1.default.green(' ' + actions.join(', ')));
4005
+ }
4006
+ }
4007
+ if (type === 'auto' || type === 'workflow') {
4008
+ console.log(chalk_1.default.bold.yellow('\nWORKFLOW SCHEMAS:'));
4009
+ console.log(chalk_1.default.gray('─'.repeat(50)));
4010
+ // Workflow core
4011
+ const workflowsDir = path.join(schemasPath, 'workflows');
4012
+ if (fs.existsSync(workflowsDir)) {
4013
+ console.log(chalk_1.default.bold('\n Core:'));
4014
+ const core = fs.readdirSync(workflowsDir)
4015
+ .filter(f => f.endsWith('.json'))
4016
+ .map(f => f.replace('.json', ''));
4017
+ console.log(chalk_1.default.green(' ' + core.join(', ')));
4018
+ // Tasks
4019
+ const tasksDir = path.join(workflowsDir, 'tasks');
4020
+ if (fs.existsSync(tasksDir)) {
4021
+ console.log(chalk_1.default.bold('\n Tasks:'));
4022
+ const tasks = fs.readdirSync(tasksDir)
4023
+ .filter(f => f.endsWith('.json'))
4024
+ .map(f => f.replace('.json', ''));
4025
+ console.log(chalk_1.default.green(' ' + tasks.join(', ')));
4026
+ }
4027
+ // Common
4028
+ const commonDir = path.join(workflowsDir, 'common');
4029
+ if (fs.existsSync(commonDir)) {
4030
+ console.log(chalk_1.default.bold('\n Common Definitions:'));
4031
+ const common = fs.readdirSync(commonDir)
4032
+ .filter(f => f.endsWith('.json'))
4033
+ .map(f => f.replace('.json', ''));
4034
+ console.log(chalk_1.default.green(' ' + common.join(', ')));
4035
+ }
4036
+ }
4037
+ }
4038
+ console.log(chalk_1.default.gray('\n─'.repeat(50)));
4039
+ console.log(chalk_1.default.gray(`\nUse '${PROGRAM_NAME} schema <name>' to view a specific schema`));
4040
+ console.log(chalk_1.default.gray(`Use '${PROGRAM_NAME} example <name>' to see an example\n`));
4041
+ }
4042
+ // ============================================================================
4043
+ // Error Formatting
4044
+ // ============================================================================
4045
+ function getSchemaSnippet(schemasPath, error) {
4046
+ if (!error.schemaPath)
4047
+ return null;
4048
+ // Try to extract component type from path
4049
+ const pathMatch = error.path.match(/components?\[?\d*\]?\.?(\w+)?/);
4050
+ if (pathMatch && pathMatch[1]) {
4051
+ const schemaFile = findSchemaFile(schemasPath, pathMatch[1]);
4052
+ if (schemaFile) {
4053
+ try {
4054
+ const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf-8'));
4055
+ return JSON.stringify(schema, null, 2).slice(0, 500) + '...';
4056
+ }
4057
+ catch {
4058
+ return null;
4059
+ }
4060
+ }
4061
+ }
4062
+ return null;
4063
+ }
4064
+ function formatErrorPretty(error, index, schemasPath, verbose) {
4065
+ const lines = [];
4066
+ // Error header
4067
+ lines.push(chalk_1.default.red(`\n┌─ Error #${index + 1}: ${error.type.toUpperCase().replace(/_/g, ' ')}`));
4068
+ lines.push(chalk_1.default.red('│'));
4069
+ // Path
4070
+ lines.push(chalk_1.default.red('│ ') + chalk_1.default.bold('Path: ') + chalk_1.default.yellow(error.path || '/'));
4071
+ // Message
4072
+ lines.push(chalk_1.default.red('│ ') + chalk_1.default.bold('Message: ') + error.message);
4073
+ // Schema path (verbose mode)
4074
+ if (verbose && error.schemaPath) {
4075
+ lines.push(chalk_1.default.red('│ ') + chalk_1.default.bold('Schema: ') + chalk_1.default.gray(error.schemaPath));
4076
+ }
4077
+ // Example (if available)
4078
+ if (error.example !== undefined) {
4079
+ lines.push(chalk_1.default.red('│'));
4080
+ lines.push(chalk_1.default.red('│ ') + chalk_1.default.bold('Example:'));
4081
+ const exampleLines = JSON.stringify(error.example, null, 2).split('\n');
4082
+ exampleLines.forEach(line => {
4083
+ lines.push(chalk_1.default.red('│ ') + chalk_1.default.green(line));
4084
+ });
4085
+ }
4086
+ // Suggestion based on error type
4087
+ const suggestion = getSuggestion(error);
4088
+ if (suggestion) {
4089
+ lines.push(chalk_1.default.red('│'));
4090
+ lines.push(chalk_1.default.red('│ ') + chalk_1.default.bold('Suggestion: ') + chalk_1.default.cyan(suggestion));
4091
+ }
4092
+ lines.push(chalk_1.default.red('│'));
4093
+ lines.push(chalk_1.default.red('└' + '─'.repeat(60)));
4094
+ return lines.join('\n');
4095
+ }
4096
+ function getSuggestion(error) {
4097
+ switch (error.type) {
4098
+ case 'missing_property':
4099
+ const propMatch = error.message.match(/property:\s*(\w+)/i) || error.path.match(/\.(\w+)$/);
4100
+ if (propMatch) {
4101
+ return `Add the required property '${propMatch[1]}' to your YAML`;
4102
+ }
4103
+ return 'Check required properties in the schema';
4104
+ case 'schema_violation':
4105
+ if (error.message.includes('enum')) {
4106
+ return 'The value must be one of the allowed values. Check the schema for valid options.';
4107
+ }
4108
+ if (error.message.includes('type')) {
4109
+ return 'Check that the value type matches the expected type (string, number, boolean, etc.)';
4110
+ }
4111
+ if (error.message.includes('additionalProperties')) {
4112
+ return 'Remove unrecognized properties. Use `cxtms schema <type>` to see allowed properties.';
4113
+ }
4114
+ return 'Review the schema requirements for this property';
4115
+ case 'yaml_syntax_error':
4116
+ return 'Check YAML indentation and syntax. Use a YAML linter to identify issues.';
4117
+ case 'invalid_task_type':
4118
+ return `Use 'cxtms list --type workflow' to see available task types`;
4119
+ case 'invalid_activity':
4120
+ return 'Each activity must have a "name" and "steps" array';
4121
+ default:
4122
+ return null;
4123
+ }
4124
+ }
4125
+ function formatWarningPretty(warning, index) {
4126
+ const lines = [];
4127
+ lines.push(chalk_1.default.yellow(`\n⚠ Warning #${index + 1}: ${warning.type.toUpperCase().replace(/_/g, ' ')}`));
4128
+ lines.push(chalk_1.default.gray(` Path: ${warning.path}`));
4129
+ lines.push(` ${warning.message}`);
4130
+ return lines.join('\n');
4131
+ }
4132
+ // ============================================================================
4133
+ // Result Output
4134
+ // ============================================================================
4135
+ function printResultPretty(result, fileType, schemasPath, verbose) {
4136
+ const { summary, errors, warnings } = result;
4137
+ // Header
4138
+ console.log('\n' + chalk_1.default.bold.cyan('╔═══════════════════════════════════════════════════════════════════╗'));
4139
+ console.log(chalk_1.default.bold.cyan('║') + chalk_1.default.bold.white(' CX SCHEMA VALIDATION REPORT ') + chalk_1.default.bold.cyan('║'));
4140
+ console.log(chalk_1.default.bold.cyan('╚═══════════════════════════════════════════════════════════════════╝\n'));
4141
+ // Summary
4142
+ console.log(chalk_1.default.bold(' File: ') + summary.file);
4143
+ console.log(chalk_1.default.bold(' Type: ') + chalk_1.default.cyan(fileType === 'auto' ? 'auto-detected' : fileType));
4144
+ console.log(chalk_1.default.bold(' Time: ') + chalk_1.default.gray(summary.timestamp));
4145
+ console.log(chalk_1.default.bold(' Status: ') +
4146
+ (summary.status === 'PASSED'
4147
+ ? chalk_1.default.green.bold('✓ PASSED')
4148
+ : chalk_1.default.red.bold('✗ FAILED')));
4149
+ console.log(chalk_1.default.bold(' Errors: ') + (summary.errorCount > 0 ? chalk_1.default.red(summary.errorCount) : chalk_1.default.green('0')));
4150
+ console.log(chalk_1.default.bold(' Warnings:') + (summary.warningCount > 0 ? chalk_1.default.yellow(summary.warningCount) : chalk_1.default.green('0')));
4151
+ // Error breakdown
4152
+ if (summary.errorCount > 0) {
4153
+ console.log('\n' + chalk_1.default.bold(' Errors by Type:'));
4154
+ for (const [type, count] of Object.entries(summary.errorsByType)) {
4155
+ console.log(chalk_1.default.gray(` ${type}: `) + chalk_1.default.red(count));
4156
+ }
4157
+ }
4158
+ // Errors
4159
+ if (errors.length > 0) {
4160
+ console.log('\n' + chalk_1.default.bold.red('═══════════════════════════════════════════════════════════════════'));
4161
+ console.log(chalk_1.default.bold.red(' ERRORS'));
4162
+ console.log(chalk_1.default.bold.red('═══════════════════════════════════════════════════════════════════'));
4163
+ errors.forEach((error, index) => {
4164
+ console.log(formatErrorPretty(error, index, schemasPath, verbose));
4165
+ });
4166
+ }
4167
+ // Warnings
4168
+ if (warnings.length > 0) {
4169
+ console.log('\n' + chalk_1.default.bold.yellow('═══════════════════════════════════════════════════════════════════'));
4170
+ console.log(chalk_1.default.bold.yellow(' WARNINGS'));
4171
+ console.log(chalk_1.default.bold.yellow('═══════════════════════════════════════════════════════════════════'));
4172
+ warnings.forEach((warning, index) => {
4173
+ console.log(formatWarningPretty(warning, index));
4174
+ });
4175
+ }
4176
+ // Footer with help
4177
+ if (errors.length > 0) {
4178
+ console.log('\n' + chalk_1.default.gray('─'.repeat(70)));
4179
+ console.log(chalk_1.default.gray(' Tips:'));
4180
+ console.log(chalk_1.default.gray(` • Use '${PROGRAM_NAME} schema <name>' to view schema requirements`));
4181
+ console.log(chalk_1.default.gray(` • Use '${PROGRAM_NAME} example <name>' to see example YAML`));
4182
+ console.log(chalk_1.default.gray(` • Use '${PROGRAM_NAME} list' to see all available schemas`));
4183
+ console.log(chalk_1.default.gray('─'.repeat(70)));
4184
+ }
4185
+ console.log('');
4186
+ }
4187
+ function printResultCompact(result, filePath) {
4188
+ const status = result.isValid ? chalk_1.default.green('PASS') : chalk_1.default.red('FAIL');
4189
+ const errorInfo = result.summary.errorCount > 0 ? chalk_1.default.red(` (${result.summary.errorCount} errors)`) : '';
4190
+ console.log(`${status} ${filePath}${errorInfo}`);
4191
+ }
4192
+ function printResultJson(result) {
4193
+ console.log(JSON.stringify(result, null, 2));
4194
+ }
4195
+ // ============================================================================
4196
+ // Report Generation
4197
+ // ============================================================================
4198
+ function buildReportData(results) {
4199
+ const errorsByType = {};
4200
+ const errorsByFile = {};
4201
+ let totalErrors = 0;
4202
+ let totalWarnings = 0;
4203
+ for (const fileResult of results) {
4204
+ const errorCount = fileResult.result.errors.length;
4205
+ totalErrors += errorCount;
4206
+ totalWarnings += fileResult.result.warnings.length;
4207
+ if (errorCount > 0) {
4208
+ errorsByFile[fileResult.file] = errorCount;
4209
+ }
4210
+ for (const error of fileResult.result.errors) {
4211
+ const type = error.type || 'unknown';
4212
+ errorsByType[type] = (errorsByType[type] || 0) + 1;
4213
+ }
4214
+ }
4215
+ return {
4216
+ timestamp: new Date().toISOString(),
4217
+ totalFiles: results.length,
4218
+ passedFiles: results.filter(r => r.result.isValid).length,
4219
+ failedFiles: results.filter(r => !r.result.isValid).length,
4220
+ totalErrors,
4221
+ totalWarnings,
4222
+ errorsByType,
4223
+ errorsByFile,
4224
+ files: results
4225
+ };
4226
+ }
4227
+ function generateJsonReport(data) {
4228
+ return JSON.stringify(data, null, 2);
4229
+ }
4230
+ function generateMarkdownReport(data) {
4231
+ const lines = [];
4232
+ // Header
4233
+ lines.push('# CX Schema Validation Report');
4234
+ lines.push('');
4235
+ lines.push(`Generated: ${data.timestamp}`);
4236
+ lines.push('');
4237
+ // Summary
4238
+ lines.push('## Summary');
4239
+ lines.push('');
4240
+ lines.push('| Metric | Value |');
4241
+ lines.push('|--------|-------|');
4242
+ lines.push(`| Total Files | ${data.totalFiles} |`);
4243
+ lines.push(`| Passed | ${data.passedFiles} |`);
4244
+ lines.push(`| Failed | ${data.failedFiles} |`);
4245
+ lines.push(`| Total Errors | ${data.totalErrors} |`);
4246
+ lines.push(`| Total Warnings | ${data.totalWarnings} |`);
4247
+ lines.push(`| Pass Rate | ${((data.passedFiles / data.totalFiles) * 100).toFixed(1)}% |`);
4248
+ lines.push('');
4249
+ // Errors by Type
4250
+ if (Object.keys(data.errorsByType).length > 0) {
4251
+ lines.push('## Errors by Type');
4252
+ lines.push('');
4253
+ lines.push('| Error Type | Count |');
4254
+ lines.push('|------------|-------|');
4255
+ for (const [type, count] of Object.entries(data.errorsByType).sort((a, b) => b[1] - a[1])) {
4256
+ lines.push(`| ${type} | ${count} |`);
4257
+ }
4258
+ lines.push('');
4259
+ }
4260
+ // Failed Files
4261
+ const failedFiles = data.files.filter(f => !f.result.isValid);
4262
+ if (failedFiles.length > 0) {
4263
+ lines.push('## Failed Files');
4264
+ lines.push('');
4265
+ for (const fileResult of failedFiles) {
4266
+ lines.push(`### ${fileResult.file}`);
4267
+ lines.push('');
4268
+ lines.push(`- **Type:** ${fileResult.fileType}`);
4269
+ lines.push(`- **Errors:** ${fileResult.result.errors.length}`);
4270
+ lines.push(`- **Warnings:** ${fileResult.result.warnings.length}`);
4271
+ lines.push('');
4272
+ if (fileResult.result.errors.length > 0) {
4273
+ lines.push('**Errors:**');
4274
+ lines.push('');
4275
+ for (const error of fileResult.result.errors) {
4276
+ lines.push(`- **${error.type}** at \`${error.path || '/'}\`: ${error.message}`);
4277
+ }
4278
+ lines.push('');
4279
+ }
4280
+ }
4281
+ }
4282
+ // Passed Files (summary)
4283
+ const passedFiles = data.files.filter(f => f.result.isValid);
4284
+ if (passedFiles.length > 0) {
4285
+ lines.push('## Passed Files');
4286
+ lines.push('');
4287
+ for (const fileResult of passedFiles) {
4288
+ const warnings = fileResult.result.warnings.length;
4289
+ lines.push(`- ✓ ${fileResult.file}${warnings > 0 ? ` (${warnings} warnings)` : ''}`);
4290
+ }
4291
+ lines.push('');
4292
+ }
4293
+ return lines.join('\n');
4294
+ }
4295
+ function generateHtmlReport(data) {
4296
+ const passRate = ((data.passedFiles / data.totalFiles) * 100).toFixed(1);
4297
+ const passRateColor = data.failedFiles === 0 ? '#22c55e' : data.passedFiles > data.failedFiles ? '#eab308' : '#ef4444';
4298
+ const errorsByTypeRows = Object.entries(data.errorsByType)
4299
+ .sort((a, b) => b[1] - a[1])
4300
+ .map(([type, count]) => `<tr><td>${type}</td><td>${count}</td></tr>`)
4301
+ .join('\n');
4302
+ const failedFilesHtml = data.files
4303
+ .filter(f => !f.result.isValid)
4304
+ .map(f => {
4305
+ const errorsHtml = f.result.errors
4306
+ .map(e => `<li><strong>${e.type}</strong> at <code>${e.path || '/'}</code>: ${escapeHtml(e.message)}</li>`)
4307
+ .join('\n');
4308
+ return `
4309
+ <div class="file-card failed">
4310
+ <h3>✗ ${escapeHtml(f.file)}</h3>
4311
+ <p><strong>Type:</strong> ${f.fileType} | <strong>Errors:</strong> ${f.result.errors.length} | <strong>Warnings:</strong> ${f.result.warnings.length}</p>
4312
+ <ul class="error-list">${errorsHtml}</ul>
4313
+ </div>
4314
+ `;
4315
+ })
4316
+ .join('\n');
4317
+ const passedFilesHtml = data.files
4318
+ .filter(f => f.result.isValid)
4319
+ .map(f => {
4320
+ const warnings = f.result.warnings.length;
4321
+ return `<div class="file-card passed"><span>✓</span> ${escapeHtml(f.file)}${warnings > 0 ? ` <span class="warning-badge">${warnings} warnings</span>` : ''}</div>`;
4322
+ })
4323
+ .join('\n');
4324
+ return `<!DOCTYPE html>
4325
+ <html lang="en">
4326
+ <head>
4327
+ <meta charset="UTF-8">
4328
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4329
+ <title>CX Schema Validation Report</title>
4330
+ <style>
4331
+ * { box-sizing: border-box; margin: 0; padding: 0; }
4332
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: #f5f5f5; color: #333; line-height: 1.6; }
4333
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
4334
+ h1 { color: #1e40af; margin-bottom: 10px; }
4335
+ h2 { color: #374151; margin: 30px 0 15px; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px; }
4336
+ h3 { color: #4b5563; margin-bottom: 10px; }
4337
+ .timestamp { color: #6b7280; margin-bottom: 30px; }
4338
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 30px; }
4339
+ .stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; }
4340
+ .stat-card .value { font-size: 2rem; font-weight: bold; }
4341
+ .stat-card .label { color: #6b7280; font-size: 0.9rem; }
4342
+ .stat-card.passed .value { color: #22c55e; }
4343
+ .stat-card.failed .value { color: #ef4444; }
4344
+ .stat-card.rate .value { color: ${passRateColor}; }
4345
+ table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
4346
+ th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #e5e7eb; }
4347
+ th { background: #f9fafb; font-weight: 600; }
4348
+ tr:last-child td { border-bottom: none; }
4349
+ .file-card { background: white; padding: 15px 20px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
4350
+ .file-card.failed { border-left: 4px solid #ef4444; }
4351
+ .file-card.passed { border-left: 4px solid #22c55e; }
4352
+ .file-card.passed span { color: #22c55e; font-weight: bold; }
4353
+ .error-list { margin-top: 10px; padding-left: 20px; }
4354
+ .error-list li { margin-bottom: 8px; font-size: 0.9rem; }
4355
+ .error-list code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-size: 0.85rem; }
4356
+ .warning-badge { background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; margin-left: 10px; }
4357
+ </style>
4358
+ </head>
4359
+ <body>
4360
+ <div class="container">
4361
+ <h1>CX Schema Validation Report</h1>
4362
+ <p class="timestamp">Generated: ${data.timestamp}</p>
4363
+
4364
+ <div class="summary-grid">
4365
+ <div class="stat-card">
4366
+ <div class="value">${data.totalFiles}</div>
4367
+ <div class="label">Total Files</div>
4368
+ </div>
4369
+ <div class="stat-card passed">
4370
+ <div class="value">${data.passedFiles}</div>
4371
+ <div class="label">Passed</div>
4372
+ </div>
4373
+ <div class="stat-card failed">
4374
+ <div class="value">${data.failedFiles}</div>
4375
+ <div class="label">Failed</div>
4376
+ </div>
4377
+ <div class="stat-card">
4378
+ <div class="value">${data.totalErrors}</div>
4379
+ <div class="label">Total Errors</div>
4380
+ </div>
4381
+ <div class="stat-card rate">
4382
+ <div class="value">${passRate}%</div>
4383
+ <div class="label">Pass Rate</div>
4384
+ </div>
4385
+ </div>
4386
+
4387
+ ${Object.keys(data.errorsByType).length > 0 ? `
4388
+ <h2>Errors by Type</h2>
4389
+ <table>
4390
+ <thead><tr><th>Error Type</th><th>Count</th></tr></thead>
4391
+ <tbody>${errorsByTypeRows}</tbody>
4392
+ </table>
4393
+ ` : ''}
4394
+
4395
+ ${data.failedFiles > 0 ? `
4396
+ <h2>Failed Files (${data.failedFiles})</h2>
4397
+ ${failedFilesHtml}
4398
+ ` : ''}
4399
+
4400
+ ${data.passedFiles > 0 ? `
4401
+ <h2>Passed Files (${data.passedFiles})</h2>
4402
+ ${passedFilesHtml}
4403
+ ` : ''}
4404
+ </div>
4405
+ </body>
4406
+ </html>`;
4407
+ }
4408
+ function escapeHtml(text) {
4409
+ return text
4410
+ .replace(/&/g, '&amp;')
4411
+ .replace(/</g, '&lt;')
4412
+ .replace(/>/g, '&gt;')
4413
+ .replace(/"/g, '&quot;')
4414
+ .replace(/'/g, '&#039;');
4415
+ }
4416
+ function detectReportFormat(filePath) {
4417
+ const ext = path.extname(filePath).toLowerCase();
4418
+ switch (ext) {
4419
+ case '.html':
4420
+ case '.htm':
4421
+ return 'html';
4422
+ case '.md':
4423
+ case '.markdown':
4424
+ return 'markdown';
4425
+ case '.json':
4426
+ default:
4427
+ return 'json';
4428
+ }
4429
+ }
4430
+ function generateReport(data, format) {
4431
+ switch (format) {
4432
+ case 'html':
4433
+ return generateHtmlReport(data);
4434
+ case 'markdown':
4435
+ return generateMarkdownReport(data);
4436
+ case 'json':
4437
+ default:
4438
+ return generateJsonReport(data);
4439
+ }
4440
+ }
4441
+ // ============================================================================
4442
+ // Validation
4443
+ // ============================================================================
4444
+ async function validateFile(filePath, options, schemasPath) {
4445
+ // Determine file type
4446
+ let fileType = options.type;
4447
+ if (fileType === 'auto') {
4448
+ fileType = detectFileType(filePath);
4449
+ }
4450
+ // Create appropriate validator
4451
+ if (fileType === 'workflow') {
4452
+ const validator = new workflowValidator_1.WorkflowValidator({
4453
+ schemasPath: path.join(schemasPath, 'workflows')
4454
+ });
4455
+ return validator.validateWorkflow(filePath);
4456
+ }
4457
+ else {
4458
+ const validator = new validator_1.ModuleValidator({ schemasPath });
4459
+ return validator.validateModule(filePath);
4460
+ }
4461
+ }
4462
+ // ============================================================================
4463
+ // Main
4464
+ // ============================================================================
4465
+ async function main() {
4466
+ const args = process.argv.slice(2);
4467
+ const { command, files, options } = parseArgs(args);
4468
+ // Handle help
4469
+ if (options.help) {
4470
+ if (command === 'schema') {
4471
+ console.log(SCHEMA_HELP);
4472
+ }
4473
+ else if (command === 'list') {
4474
+ console.log(LIST_HELP);
4475
+ }
4476
+ else if (command === 'init') {
4477
+ console.log(INIT_HELP);
4478
+ }
4479
+ else if (command === 'create') {
4480
+ console.log(CREATE_HELP);
4481
+ }
4482
+ else if (command === 'extract') {
4483
+ console.log(EXTRACT_HELP);
4484
+ }
4485
+ else {
4486
+ console.log(HELP_TEXT);
4487
+ }
4488
+ process.exit(0);
4489
+ }
4490
+ // Handle version
4491
+ if (options.version) {
4492
+ console.log(`cxtms v${VERSION}`);
4493
+ process.exit(0);
4494
+ }
4495
+ // Handle login command (no schemas needed)
4496
+ if (command === 'login') {
4497
+ if (!files[0]) {
4498
+ console.error(chalk_1.default.red('Error: URL required'));
4499
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} login <url>`));
4500
+ process.exit(2);
4501
+ }
4502
+ await runLogin(files[0]);
4503
+ process.exit(0);
4504
+ }
4505
+ // Handle logout command (no schemas needed)
4506
+ if (command === 'logout') {
4507
+ await runLogout(files[0]);
4508
+ process.exit(0);
4509
+ }
4510
+ // Handle pat command (no schemas needed)
4511
+ if (command === 'pat') {
4512
+ const sub = files[0];
4513
+ if (sub === 'create') {
4514
+ if (!files[1]) {
4515
+ console.error(chalk_1.default.red('Error: Token name required'));
4516
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat create <name>`));
4517
+ process.exit(2);
4518
+ }
4519
+ await runPatCreate(files[1]);
4520
+ }
4521
+ else if (sub === 'list' || !sub) {
4522
+ await runPatList();
4523
+ }
4524
+ else if (sub === 'revoke') {
4525
+ if (!files[1]) {
4526
+ console.error(chalk_1.default.red('Error: Token ID required'));
4527
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat revoke <tokenId>`));
4528
+ process.exit(2);
4529
+ }
4530
+ await runPatRevoke(files[1]);
4531
+ }
4532
+ else if (sub === 'setup') {
4533
+ await runPatSetup();
4534
+ }
4535
+ else {
4536
+ console.error(chalk_1.default.red(`Unknown pat subcommand: ${sub}`));
4537
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat <create|list|revoke|setup>`));
4538
+ process.exit(2);
4539
+ }
4540
+ process.exit(0);
4541
+ }
4542
+ // Handle orgs command (no schemas needed)
4543
+ if (command === 'orgs') {
4544
+ const sub = files[0];
4545
+ if (sub === 'list' || !sub) {
4546
+ await runOrgsList();
4547
+ }
4548
+ else if (sub === 'use') {
4549
+ await runOrgsUse(files[1]);
4550
+ }
4551
+ else if (sub === 'select') {
4552
+ await runOrgsSelect();
4553
+ }
4554
+ else {
4555
+ console.error(chalk_1.default.red(`Unknown orgs subcommand: ${sub}`));
4556
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} orgs <list|use|select>`));
4557
+ process.exit(2);
4558
+ }
4559
+ process.exit(0);
4560
+ }
4561
+ // Handle appmodule command (no schemas needed)
4562
+ if (command === 'appmodule') {
4563
+ const sub = files[0];
4564
+ if (sub === 'deploy') {
4565
+ await runAppModuleDeploy(files[1], options.orgId);
4566
+ }
4567
+ else if (sub === 'undeploy') {
4568
+ await runAppModuleUndeploy(files[1], options.orgId);
4569
+ }
4570
+ else {
4571
+ console.error(chalk_1.default.red(`Unknown appmodule subcommand: ${sub || '(none)'}`));
4572
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule <deploy|undeploy> ...`));
4573
+ process.exit(2);
4574
+ }
4575
+ process.exit(0);
4576
+ }
4577
+ // Handle workflow command (no schemas needed)
4578
+ if (command === 'workflow') {
4579
+ const sub = files[0];
4580
+ if (sub === 'deploy') {
4581
+ await runWorkflowDeploy(files[1], options.orgId);
4582
+ }
4583
+ else if (sub === 'undeploy') {
4584
+ await runWorkflowUndeploy(files[1], options.orgId);
4585
+ }
4586
+ else if (sub === 'execute') {
4587
+ await runWorkflowExecute(files[1], options.orgId, options.vars, options.file);
4588
+ }
4589
+ else if (sub === 'logs') {
4590
+ await runWorkflowLogs(files[1], options.orgId, options.from, options.to);
4591
+ }
4592
+ else if (sub === 'log') {
4593
+ await runWorkflowLog(files[1], options.orgId, options.output, options.console, options.format === 'json');
4594
+ }
4595
+ else {
4596
+ console.error(chalk_1.default.red(`Unknown workflow subcommand: ${sub || '(none)'}`));
4597
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <deploy|undeploy|execute|logs|log> ...`));
4598
+ process.exit(2);
4599
+ }
4600
+ process.exit(0);
4601
+ }
4602
+ // Handle publish command (no schemas needed)
4603
+ if (command === 'publish') {
4604
+ await runPublish(files[0] || options.feature, options.orgId);
4605
+ process.exit(0);
4606
+ }
4607
+ // Handle app command (no schemas needed)
4608
+ if (command === 'app') {
4609
+ const sub = files[0];
4610
+ if (sub === 'install' || sub === 'upgrade') {
4611
+ await runAppInstall(options.orgId, options.branch, options.force, options.skipChanged);
4612
+ }
4613
+ else if (sub === 'release' || sub === 'publish') {
4614
+ await runAppPublish(options.orgId, options.message, options.branch, options.force, files.slice(1));
4615
+ }
4616
+ else if (sub === 'list' || !sub) {
4617
+ await runAppList(options.orgId);
4618
+ }
4619
+ else {
4620
+ console.error(chalk_1.default.red(`Unknown app subcommand: ${sub}`));
4621
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} app <install|upgrade|release|list>`));
4622
+ process.exit(2);
4623
+ }
4624
+ process.exit(0);
4625
+ }
4626
+ // Handle gql command (no schemas needed)
4627
+ if (command === 'gql') {
4628
+ const sub = files[0];
4629
+ // For 'gql type <name>', the type name is in files[1] — use it as filter
4630
+ const filterArg = sub === 'type' ? (files[1] || options.filter) : options.filter;
4631
+ await runGql(sub, filterArg);
4632
+ process.exit(0);
4633
+ }
4634
+ // Handle query command (no schemas needed)
4635
+ if (command === 'query') {
4636
+ await runQuery(files[0], options.vars);
4637
+ process.exit(0);
4638
+ }
4639
+ // Find schemas path
4640
+ const schemasPath = options.schemasPath || findSchemasPath();
4641
+ if (!schemasPath) {
4642
+ console.error(chalk_1.default.red('Error: Could not find schemas directory.'));
4643
+ console.error(chalk_1.default.gray('Please run npm install first or specify --schemas <path>'));
4644
+ process.exit(2);
4645
+ }
4646
+ // Handle schema command
4647
+ if (options.showSchema) {
4648
+ showSchema(schemasPath, options.showSchema);
4649
+ process.exit(0);
4650
+ }
4651
+ // Handle example command
4652
+ if (options.showExample) {
4653
+ showExample(schemasPath, options.showExample);
4654
+ process.exit(0);
4655
+ }
4656
+ // Handle list command
4657
+ if (options.listSchemas) {
4658
+ listSchemas(schemasPath, options.type);
4659
+ process.exit(0);
4660
+ }
4661
+ // Handle init command
4662
+ if (command === 'init') {
4663
+ runInit(files[0]);
4664
+ process.exit(0);
4665
+ }
4666
+ // Handle create command
4667
+ if (command === 'create') {
4668
+ // For task-schema, pass --tasks (from options.createTasks) as the tasks argument
4669
+ if (files[0] === 'task-schema') {
4670
+ runCreate(files[0], files[1], options.template, options.feature, options.createTasks);
4671
+ }
4672
+ else {
4673
+ runCreate(files[0], files[1], options.template, options.feature, options.createOptions);
4674
+ }
4675
+ process.exit(0);
4676
+ }
4677
+ // Handle sync-schemas command
4678
+ if (command === 'sync-schemas') {
4679
+ runSyncSchemas();
4680
+ process.exit(0);
4681
+ }
4682
+ // Handle install-skills command
4683
+ if (command === 'install-skills') {
4684
+ runInstallSkills();
4685
+ process.exit(0);
4686
+ }
4687
+ // Handle update command
4688
+ if (command === 'update') {
4689
+ runUpdate();
4690
+ process.exit(0);
4691
+ }
4692
+ // Handle setup-claude command
4693
+ if (command === 'setup-claude') {
4694
+ runSetupClaude();
4695
+ process.exit(0);
4696
+ }
4697
+ // Handle extract command
4698
+ if (command === 'extract') {
4699
+ runExtract(files[0], files[1], options.extractTo, options.extractCopy);
4700
+ process.exit(0);
4701
+ }
4702
+ // Validate files
4703
+ if (files.length === 0) {
4704
+ console.error(chalk_1.default.red('Error: No input file specified'));
4705
+ console.error(chalk_1.default.gray(`Use '${PROGRAM_NAME} --help' for usage information`));
4706
+ process.exit(2);
4707
+ }
4708
+ let hasErrors = false;
4709
+ const allResults = [];
4710
+ const isReportMode = command === 'report' || options.report;
4711
+ for (const file of files) {
4712
+ // Check file exists
4713
+ if (!fs.existsSync(file)) {
4714
+ console.error(chalk_1.default.red(`Error: File not found: ${file}`));
4715
+ hasErrors = true;
4716
+ continue;
4717
+ }
4718
+ try {
4719
+ const fileType = options.type === 'auto' ? detectFileType(file) : options.type;
4720
+ const result = await validateFile(file, options, schemasPath);
4721
+ // Collect results for report
4722
+ if (isReportMode) {
4723
+ allResults.push({ file, fileType, result });
4724
+ }
4725
+ // Output individual results (unless quiet mode for reports)
4726
+ if (!isReportMode || !options.quiet) {
4727
+ if (options.format === 'json' && !isReportMode) {
4728
+ printResultJson(result);
4729
+ }
4730
+ else if (options.format === 'compact' || isReportMode) {
4731
+ printResultCompact(result, file);
4732
+ }
4733
+ else {
4734
+ printResultPretty(result, fileType, schemasPath, options.verbose);
4735
+ }
4736
+ }
4737
+ if (!result.isValid) {
4738
+ hasErrors = true;
4739
+ }
4740
+ }
4741
+ catch (error) {
4742
+ console.error(chalk_1.default.red(`Error validating ${file}:`), error.message);
4743
+ if (options.verbose) {
4744
+ console.error(chalk_1.default.gray(error.stack));
4745
+ }
4746
+ hasErrors = true;
4747
+ }
4748
+ }
4749
+ // Generate report if requested
4750
+ if (isReportMode && allResults.length > 0) {
4751
+ const reportData = buildReportData(allResults);
4752
+ const reportPath = options.report || 'validation-report.json';
4753
+ // Determine report format (auto-detect from extension if not specified explicitly)
4754
+ let reportFormat = options.reportFormat;
4755
+ if (!options.reportFormat || options.reportFormat === 'json') {
4756
+ // If reportFormat wasn't explicitly set, auto-detect from file extension
4757
+ const detectedFormat = detectReportFormat(reportPath);
4758
+ if (detectedFormat !== 'json' || !options.reportFormat) {
4759
+ reportFormat = detectedFormat;
4760
+ }
4761
+ }
4762
+ const reportContent = generateReport(reportData, reportFormat);
4763
+ fs.writeFileSync(reportPath, reportContent, 'utf-8');
4764
+ // Print summary
4765
+ console.log('');
4766
+ console.log(chalk_1.default.bold.cyan('═══════════════════════════════════════════════════════════════════'));
4767
+ console.log(chalk_1.default.bold.cyan(' VALIDATION SUMMARY'));
4768
+ console.log(chalk_1.default.bold.cyan('═══════════════════════════════════════════════════════════════════'));
4769
+ console.log('');
4770
+ console.log(chalk_1.default.bold(' Total Files: ') + reportData.totalFiles);
4771
+ console.log(chalk_1.default.bold(' Passed: ') + chalk_1.default.green(reportData.passedFiles));
4772
+ console.log(chalk_1.default.bold(' Failed: ') + (reportData.failedFiles > 0 ? chalk_1.default.red(reportData.failedFiles) : '0'));
4773
+ console.log(chalk_1.default.bold(' Pass Rate: ') + chalk_1.default.cyan(`${((reportData.passedFiles / reportData.totalFiles) * 100).toFixed(1)}%`));
4774
+ console.log('');
4775
+ console.log(chalk_1.default.gray(` Report saved to: ${chalk_1.default.white(reportPath)}`));
4776
+ console.log('');
4777
+ }
4778
+ process.exit(hasErrors ? 1 : 0);
4779
+ }
4780
+ main().catch(error => {
4781
+ console.error(chalk_1.default.red('Unexpected error:'), error.message);
4782
+ process.exit(2);
4783
+ });
4784
+ //# sourceMappingURL=cli.js.map