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.
- package/README.md +384 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4784 -0
- package/dist/cli.js.map +1 -0
- package/dist/extractUtils.d.ts +11 -0
- package/dist/extractUtils.d.ts.map +1 -0
- package/dist/extractUtils.js +19 -0
- package/dist/extractUtils.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/schemaLoader.d.ts +17 -0
- package/dist/utils/schemaLoader.d.ts.map +1 -0
- package/dist/utils/schemaLoader.js +134 -0
- package/dist/utils/schemaLoader.js.map +1 -0
- package/dist/validator.d.ts +72 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +432 -0
- package/dist/validator.js.map +1 -0
- package/dist/workflowValidator.d.ts +103 -0
- package/dist/workflowValidator.d.ts.map +1 -0
- package/dist/workflowValidator.js +753 -0
- package/dist/workflowValidator.js.map +1 -0
- package/package.json +51 -0
- package/schemas/actions/all.json +27 -0
- package/schemas/actions/clipboard.json +46 -0
- package/schemas/actions/confirm.json +21 -0
- package/schemas/actions/consoleLog.json +16 -0
- package/schemas/actions/dialog.json +25 -0
- package/schemas/actions/fileDownload.json +16 -0
- package/schemas/actions/forEach.json +31 -0
- package/schemas/actions/if.json +12 -0
- package/schemas/actions/mutation.json +25 -0
- package/schemas/actions/navigate.json +18 -0
- package/schemas/actions/navigateBack.json +22 -0
- package/schemas/actions/navigateBackOrClose.json +21 -0
- package/schemas/actions/notification.json +19 -0
- package/schemas/actions/openBarcodeScanner.json +104 -0
- package/schemas/actions/query.json +32 -0
- package/schemas/actions/refresh.json +13 -0
- package/schemas/actions/resetDirtyState.json +22 -0
- package/schemas/actions/setFields.json +21 -0
- package/schemas/actions/setStore.json +13 -0
- package/schemas/actions/validateForm.json +15 -0
- package/schemas/actions/workflow.json +24 -0
- package/schemas/components/README.md +147 -0
- package/schemas/components/appComponent.json +58 -0
- package/schemas/components/barcodeScanner.json +69 -0
- package/schemas/components/button.json +123 -0
- package/schemas/components/calendar.json +489 -0
- package/schemas/components/card.json +176 -0
- package/schemas/components/collection.json +54 -0
- package/schemas/components/dataGrid.json +119 -0
- package/schemas/components/datasource.json +151 -0
- package/schemas/components/dropdown.json +57 -0
- package/schemas/components/field-collection.json +618 -0
- package/schemas/components/field.json +265 -0
- package/schemas/components/form.json +234 -0
- package/schemas/components/index.json +71 -0
- package/schemas/components/layout.json +69 -0
- package/schemas/components/module.json +167 -0
- package/schemas/components/navDropdown.json +36 -0
- package/schemas/components/navbar.json +78 -0
- package/schemas/components/navbarItem.json +28 -0
- package/schemas/components/navbarLink.json +36 -0
- package/schemas/components/row.json +31 -0
- package/schemas/components/slot.json +30 -0
- package/schemas/components/tab.json +34 -0
- package/schemas/components/tabs.json +35 -0
- package/schemas/components/timeline.json +172 -0
- package/schemas/components/timelineGrid.json +328 -0
- package/schemas/fields/README.md +66 -0
- package/schemas/fields/attachment.json +156 -0
- package/schemas/fields/autocomplete-googleplaces.json +130 -0
- package/schemas/fields/checkbox.json +82 -0
- package/schemas/fields/date.json +88 -0
- package/schemas/fields/datetime.json +75 -0
- package/schemas/fields/email.json +75 -0
- package/schemas/fields/index.json +53 -0
- package/schemas/fields/number.json +91 -0
- package/schemas/fields/password.json +70 -0
- package/schemas/fields/radio.json +94 -0
- package/schemas/fields/rangedatetime.json +56 -0
- package/schemas/fields/select-async.json +334 -0
- package/schemas/fields/select.json +115 -0
- package/schemas/fields/tel.json +79 -0
- package/schemas/fields/text.json +86 -0
- package/schemas/fields/textarea.json +95 -0
- package/schemas/fields/time.json +91 -0
- package/schemas/fields/url.json +74 -0
- package/schemas/schema.graphql +12248 -0
- package/schemas/schemas.json +610 -0
- package/schemas/workflows/activity.json +96 -0
- package/schemas/workflows/common/condition.json +48 -0
- package/schemas/workflows/common/expression.json +76 -0
- package/schemas/workflows/common/mapping.json +173 -0
- package/schemas/workflows/common/step.json +38 -0
- package/schemas/workflows/flow/aggregation.json +44 -0
- package/schemas/workflows/flow/entity.json +129 -0
- package/schemas/workflows/flow/state.json +105 -0
- package/schemas/workflows/flow/transition.json +143 -0
- package/schemas/workflows/input.json +122 -0
- package/schemas/workflows/output.json +61 -0
- package/schemas/workflows/schedule.json +26 -0
- package/schemas/workflows/tasks/accounting-transaction.json +95 -0
- package/schemas/workflows/tasks/action-event.json +65 -0
- package/schemas/workflows/tasks/all.json +152 -0
- package/schemas/workflows/tasks/appmodule.json +56 -0
- package/schemas/workflows/tasks/attachment.json +97 -0
- package/schemas/workflows/tasks/authentication.json +86 -0
- package/schemas/workflows/tasks/caching.json +68 -0
- package/schemas/workflows/tasks/charge.json +92 -0
- package/schemas/workflows/tasks/commodity.json +92 -0
- package/schemas/workflows/tasks/contact-address.json +72 -0
- package/schemas/workflows/tasks/contact-payment-method.json +72 -0
- package/schemas/workflows/tasks/contact.json +82 -0
- package/schemas/workflows/tasks/csv.json +81 -0
- package/schemas/workflows/tasks/document-render.json +105 -0
- package/schemas/workflows/tasks/document-send.json +84 -0
- package/schemas/workflows/tasks/edi.json +157 -0
- package/schemas/workflows/tasks/email-send.json +110 -0
- package/schemas/workflows/tasks/error.json +72 -0
- package/schemas/workflows/tasks/export.json +90 -0
- package/schemas/workflows/tasks/filetransfer.json +102 -0
- package/schemas/workflows/tasks/flow-transition.json +68 -0
- package/schemas/workflows/tasks/foreach.json +69 -0
- package/schemas/workflows/tasks/generic.json +47 -0
- package/schemas/workflows/tasks/graphql.json +78 -0
- package/schemas/workflows/tasks/httpRequest.json +161 -0
- package/schemas/workflows/tasks/import.json +64 -0
- package/schemas/workflows/tasks/inventory.json +67 -0
- package/schemas/workflows/tasks/job.json +88 -0
- package/schemas/workflows/tasks/log.json +73 -0
- package/schemas/workflows/tasks/map.json +58 -0
- package/schemas/workflows/tasks/movement.json +54 -0
- package/schemas/workflows/tasks/note.json +59 -0
- package/schemas/workflows/tasks/number.json +65 -0
- package/schemas/workflows/tasks/order-tracking-event.json +109 -0
- package/schemas/workflows/tasks/order.json +139 -0
- package/schemas/workflows/tasks/payment.json +85 -0
- package/schemas/workflows/tasks/pdf-document.json +60 -0
- package/schemas/workflows/tasks/postal-codes.json +92 -0
- package/schemas/workflows/tasks/resolve-timezone.json +65 -0
- package/schemas/workflows/tasks/setVariable.json +76 -0
- package/schemas/workflows/tasks/switch.json +75 -0
- package/schemas/workflows/tasks/template.json +73 -0
- package/schemas/workflows/tasks/tracking-event.json +137 -0
- package/schemas/workflows/tasks/transmission.json +185 -0
- package/schemas/workflows/tasks/unzip-file.json +68 -0
- package/schemas/workflows/tasks/user.json +70 -0
- package/schemas/workflows/tasks/validation.json +99 -0
- package/schemas/workflows/tasks/while.json +53 -0
- package/schemas/workflows/tasks/workflow-execute.json +82 -0
- package/schemas/workflows/trigger.json +90 -0
- package/schemas/workflows/variable.json +46 -0
- package/schemas/workflows/workflow.json +335 -0
- package/scripts/postinstall.js +291 -0
- package/scripts/setup-vscode.js +80 -0
- package/skills/cxtms-developer/SKILL.md +118 -0
- package/skills/cxtms-developer/ref-cli-auth.md +120 -0
- package/skills/cxtms-developer/ref-entity-accounting.md +180 -0
- package/skills/cxtms-developer/ref-entity-commodity.md +239 -0
- package/skills/cxtms-developer/ref-entity-contact.md +163 -0
- package/skills/cxtms-developer/ref-entity-geography.md +154 -0
- package/skills/cxtms-developer/ref-entity-job.md +77 -0
- package/skills/cxtms-developer/ref-entity-notification.md +85 -0
- package/skills/cxtms-developer/ref-entity-order-sub.md +160 -0
- package/skills/cxtms-developer/ref-entity-order.md +183 -0
- package/skills/cxtms-developer/ref-entity-organization.md +41 -0
- package/skills/cxtms-developer/ref-entity-rate.md +182 -0
- package/skills/cxtms-developer/ref-entity-shared.md +176 -0
- package/skills/cxtms-developer/ref-entity-warehouse.md +115 -0
- package/skills/cxtms-developer/ref-graphql-query.md +309 -0
- package/skills/cxtms-module-builder/SKILL.md +477 -0
- package/skills/cxtms-module-builder/ref-components-data.md +293 -0
- package/skills/cxtms-module-builder/ref-components-display.md +411 -0
- package/skills/cxtms-module-builder/ref-components-forms.md +369 -0
- package/skills/cxtms-module-builder/ref-components-interactive.md +317 -0
- package/skills/cxtms-module-builder/ref-components-layout.md +390 -0
- package/skills/cxtms-module-builder/ref-components-specialized.md +477 -0
- package/skills/cxtms-workflow-builder/SKILL.md +438 -0
- package/skills/cxtms-workflow-builder/ref-accounting.md +66 -0
- package/skills/cxtms-workflow-builder/ref-communication.md +169 -0
- package/skills/cxtms-workflow-builder/ref-entity.md +342 -0
- package/skills/cxtms-workflow-builder/ref-expressions-ncalc.md +128 -0
- package/skills/cxtms-workflow-builder/ref-expressions-template.md +161 -0
- package/skills/cxtms-workflow-builder/ref-filetransfer.md +80 -0
- package/skills/cxtms-workflow-builder/ref-flow.md +210 -0
- package/skills/cxtms-workflow-builder/ref-other.md +157 -0
- package/skills/cxtms-workflow-builder/ref-query.md +105 -0
- package/skills/cxtms-workflow-builder/ref-utilities.md +417 -0
- package/templates/module-configuration.yaml +44 -0
- package/templates/module-form.yaml +152 -0
- package/templates/module-grid.yaml +229 -0
- package/templates/module-select.yaml +139 -0
- package/templates/module.yaml +84 -0
- package/templates/workflow-api-tracking.yaml +189 -0
- package/templates/workflow-basic.yaml +76 -0
- package/templates/workflow-document.yaml +155 -0
- package/templates/workflow-entity-trigger.yaml +90 -0
- package/templates/workflow-ftp-edi.yaml +158 -0
- package/templates/workflow-ftp-tracking.yaml +161 -0
- package/templates/workflow-mcp-tool.yaml +112 -0
- package/templates/workflow-public-api.yaml +135 -0
- package/templates/workflow-scheduled-execute.yaml +75 -0
- package/templates/workflow-scheduled.yaml +125 -0
- package/templates/workflow-utility.yaml +96 -0
- package/templates/workflow-webhook.yaml +128 -0
- 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, '&')
|
|
4411
|
+
.replace(/</g, '<')
|
|
4412
|
+
.replace(/>/g, '>')
|
|
4413
|
+
.replace(/"/g, '"')
|
|
4414
|
+
.replace(/'/g, ''');
|
|
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
|