@cxtms/cx-schema 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/cx-core/SKILL.md +38 -20
- package/.claude/skills/cx-core/ref-entity-accounting.md +0 -7
- package/.claude/skills/cx-core/ref-entity-commodity.md +0 -12
- package/.claude/skills/cx-core/ref-entity-contact.md +0 -10
- package/.claude/skills/cx-core/ref-entity-geography.md +1 -34
- package/.claude/skills/cx-core/ref-entity-notification.md +85 -0
- package/.claude/skills/cx-core/ref-entity-order-sub.md +7 -13
- package/.claude/skills/cx-core/ref-entity-order.md +1 -14
- package/.claude/skills/cx-core/ref-entity-rate.md +0 -8
- package/.claude/skills/cx-core/ref-entity-shared.md +20 -9
- package/.claude/skills/cx-core/ref-entity-warehouse.md +0 -5
- package/.claude/skills/cx-module/SKILL.md +28 -103
- package/.claude/skills/cx-module/ref-components-data.md +0 -7
- package/.claude/skills/cx-module/ref-components-display.md +0 -17
- package/.claude/skills/cx-module/ref-components-forms.md +2 -7
- package/.claude/skills/cx-module/ref-components-interactive.md +0 -11
- package/.claude/skills/cx-module/ref-components-layout.md +0 -11
- package/.claude/skills/cx-module/ref-components-specialized.md +0 -50
- package/.claude/skills/cx-workflow/SKILL.md +21 -130
- package/.claude/skills/cx-workflow/ref-communication.md +0 -8
- package/.claude/skills/cx-workflow/ref-entity.md +62 -43
- package/.claude/skills/cx-workflow/ref-expressions.md +272 -0
- package/.claude/skills/cx-workflow/ref-flow.md +6 -12
- package/.claude/skills/cx-workflow/ref-other.md +33 -14
- package/.claude/skills/cx-workflow/ref-utilities.md +87 -14
- package/README.md +34 -34
- package/dist/cli.js +166 -2394
- package/dist/cli.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validator.d.ts +0 -8
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +2 -54
- package/dist/validator.js.map +1 -1
- package/dist/workflowValidator.d.ts +0 -4
- package/dist/workflowValidator.d.ts.map +1 -1
- package/dist/workflowValidator.js +2 -28
- package/dist/workflowValidator.js.map +1 -1
- package/package.json +2 -3
- package/schemas/actions/all.json +2 -1
- package/schemas/actions/clipboard.json +46 -0
- package/schemas/components/appComponent.json +0 -8
- package/schemas/components/module.json +2 -31
- package/schemas/components/timelineGrid.json +0 -4
- package/schemas/schemas.json +0 -12
- package/schemas/workflows/flow/entity.json +19 -0
- package/schemas/workflows/tasks/all.json +9 -0
- package/schemas/workflows/tasks/authentication.json +12 -26
- package/schemas/workflows/tasks/edi.json +93 -1
- package/schemas/workflows/tasks/httpRequest.json +4 -0
- package/schemas/workflows/tasks/order.json +45 -0
- package/schemas/workflows/tasks/resolve-timezone.json +65 -0
- package/schemas/workflows/tasks/transmission.json +1 -1
- package/schemas/workflows/tasks/unzip-file.json +68 -0
- package/schemas/workflows/variable.json +5 -1
- package/schemas/workflows/workflow.json +4 -0
- package/scripts/postinstall.js +1 -1
- package/templates/module-configuration.yaml +89 -23
- package/templates/module-form.yaml +3 -3
- package/templates/module-grid.yaml +3 -3
- package/templates/module-select.yaml +3 -3
- package/templates/module.yaml +2 -3
- package/templates/workflow-api-tracking.yaml +1 -1
- package/templates/workflow-basic.yaml +1 -1
- package/templates/workflow-document.yaml +1 -1
- package/templates/workflow-entity-trigger.yaml +1 -1
- package/templates/workflow-ftp-edi.yaml +1 -1
- package/templates/workflow-ftp-tracking.yaml +1 -1
- package/templates/workflow-mcp-tool.yaml +1 -1
- package/templates/workflow-public-api.yaml +1 -1
- package/templates/workflow-scheduled.yaml +1 -1
- package/templates/workflow-utility.yaml +1 -1
- package/templates/workflow-webhook.yaml +1 -1
- package/templates/workflow.yaml +1 -1
- package/.claude/skills/cx-core/ref-cli-auth.md +0 -120
- package/.claude/skills/cx-core/ref-graphql-query.md +0 -320
- package/.claude/skills/cx-workflow/ref-expressions-ncalc.md +0 -112
- package/.claude/skills/cx-workflow/ref-expressions-template.md +0 -149
package/dist/cli.js
CHANGED
|
@@ -42,47 +42,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
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
45
|
const chalk_1 = __importDefault(require("chalk"));
|
|
50
46
|
const yaml_1 = __importStar(require("yaml"));
|
|
51
47
|
const validator_1 = require("./validator");
|
|
52
48
|
const workflowValidator_1 = require("./workflowValidator");
|
|
53
49
|
const extractUtils_1 = require("./extractUtils");
|
|
54
50
|
// ============================================================================
|
|
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
51
|
// Constants
|
|
83
52
|
// ============================================================================
|
|
84
53
|
const VERSION = require('../package.json').version;
|
|
85
|
-
const PROGRAM_NAME = '
|
|
54
|
+
const PROGRAM_NAME = 'cx-cli';
|
|
86
55
|
// ============================================================================
|
|
87
56
|
// Help Text
|
|
88
57
|
// ============================================================================
|
|
@@ -110,16 +79,6 @@ ${chalk_1.default.bold.yellow('COMMANDS:')}
|
|
|
110
79
|
${chalk_1.default.green('install-skills')} Install Claude Code skills into project .claude/skills/
|
|
111
80
|
${chalk_1.default.green('setup-claude')} Add CX project instructions to CLAUDE.md
|
|
112
81
|
${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
82
|
${chalk_1.default.green('schema')} Show JSON schema for a component or task
|
|
124
83
|
${chalk_1.default.green('example')} Show example YAML for a component or task
|
|
125
84
|
${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
|
|
@@ -142,17 +101,6 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
|
|
|
142
101
|
${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
|
|
143
102
|
${chalk_1.default.green('--to <file>')} Target file for extract command
|
|
144
103
|
${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
104
|
|
|
157
105
|
${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
|
|
158
106
|
${chalk_1.default.gray('# Validate a module YAML file')}
|
|
@@ -218,136 +166,6 @@ ${chalk_1.default.bold.yellow('SCHEMA COMMANDS:')}
|
|
|
218
166
|
${chalk_1.default.cyan(`${PROGRAM_NAME} list`)}
|
|
219
167
|
${chalk_1.default.cyan(`${PROGRAM_NAME} list --type workflow`)}
|
|
220
168
|
|
|
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
169
|
${chalk_1.default.bold.yellow('VALIDATION TYPES:')}
|
|
352
170
|
${chalk_1.default.bold('module')} - CargoXplorer UI module definitions (components, routes, entities)
|
|
353
171
|
${chalk_1.default.bold('workflow')} - CargoXplorer workflow definitions (activities, tasks, triggers)
|
|
@@ -364,8 +182,6 @@ ${chalk_1.default.bold.yellow('EXIT CODES:')}
|
|
|
364
182
|
${chalk_1.default.red('2')} - CLI error (invalid arguments, file not found, etc.)
|
|
365
183
|
|
|
366
184
|
${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
185
|
${chalk_1.default.green('CX_SCHEMA_PATH')} - Default path to schemas directory
|
|
370
186
|
${chalk_1.default.green('NO_COLOR')} - Disable colored output
|
|
371
187
|
|
|
@@ -511,8 +327,7 @@ function generateAppYaml(name) {
|
|
|
511
327
|
const dirName = path.basename(process.cwd());
|
|
512
328
|
const appName = name || dirName;
|
|
513
329
|
const scopedName = appName.startsWith('@') ? appName : `@cargox/${appName}`;
|
|
514
|
-
return `
|
|
515
|
-
name: "${scopedName}"
|
|
330
|
+
return `name: "${scopedName}"
|
|
516
331
|
description: ""
|
|
517
332
|
author: "CargoX"
|
|
518
333
|
version: "1.0.0"
|
|
@@ -552,39 +367,39 @@ npm install @cxtms/cx-schema
|
|
|
552
367
|
|
|
553
368
|
\`\`\`bash
|
|
554
369
|
# Validate all modules
|
|
555
|
-
npx
|
|
370
|
+
npx cx-cli modules/*.yaml
|
|
556
371
|
|
|
557
372
|
# Validate all workflows
|
|
558
|
-
npx
|
|
373
|
+
npx cx-cli workflows/*.yaml
|
|
559
374
|
|
|
560
375
|
# Validate with detailed output
|
|
561
|
-
npx
|
|
376
|
+
npx cx-cli --verbose modules/my-module.yaml
|
|
562
377
|
|
|
563
378
|
# Generate validation report
|
|
564
|
-
npx
|
|
379
|
+
npx cx-cli report modules/*.yaml workflows/*.yaml --report report.html
|
|
565
380
|
\`\`\`
|
|
566
381
|
|
|
567
382
|
### Create new files
|
|
568
383
|
|
|
569
384
|
\`\`\`bash
|
|
570
385
|
# Create a new module
|
|
571
|
-
npx
|
|
386
|
+
npx cx-cli create module my-module
|
|
572
387
|
|
|
573
388
|
# Create a new workflow
|
|
574
|
-
npx
|
|
389
|
+
npx cx-cli create workflow my-workflow
|
|
575
390
|
\`\`\`
|
|
576
391
|
|
|
577
392
|
### View schemas and examples
|
|
578
393
|
|
|
579
394
|
\`\`\`bash
|
|
580
395
|
# List available schemas
|
|
581
|
-
npx
|
|
396
|
+
npx cx-cli list
|
|
582
397
|
|
|
583
398
|
# View schema for a component
|
|
584
|
-
npx
|
|
399
|
+
npx cx-cli schema form
|
|
585
400
|
|
|
586
401
|
# View example YAML
|
|
587
|
-
npx
|
|
402
|
+
npx cx-cli example workflow
|
|
588
403
|
\`\`\`
|
|
589
404
|
|
|
590
405
|
## Documentation
|
|
@@ -605,13 +420,13 @@ When making changes to YAML files, always validate them:
|
|
|
605
420
|
|
|
606
421
|
\`\`\`bash
|
|
607
422
|
# Validate a specific module file
|
|
608
|
-
npx
|
|
423
|
+
npx cx-cli modules/<module-name>.yaml
|
|
609
424
|
|
|
610
425
|
# Validate a specific workflow file
|
|
611
|
-
npx
|
|
426
|
+
npx cx-cli workflows/<workflow-name>.yaml
|
|
612
427
|
|
|
613
428
|
# Validate all files with a report
|
|
614
|
-
npx
|
|
429
|
+
npx cx-cli report modules/*.yaml workflows/*.yaml --report validation-report.md
|
|
615
430
|
\`\`\`
|
|
616
431
|
|
|
617
432
|
## Schema Reference
|
|
@@ -620,14 +435,14 @@ Before editing components or tasks, check the schema:
|
|
|
620
435
|
|
|
621
436
|
\`\`\`bash
|
|
622
437
|
# View schema for components
|
|
623
|
-
npx
|
|
624
|
-
npx
|
|
625
|
-
npx
|
|
438
|
+
npx cx-cli schema form
|
|
439
|
+
npx cx-cli schema dataGrid
|
|
440
|
+
npx cx-cli schema layout
|
|
626
441
|
|
|
627
442
|
# View schema for workflow tasks
|
|
628
|
-
npx
|
|
629
|
-
npx
|
|
630
|
-
npx
|
|
443
|
+
npx cx-cli schema foreach
|
|
444
|
+
npx cx-cli schema graphql
|
|
445
|
+
npx cx-cli schema switch
|
|
631
446
|
\`\`\`
|
|
632
447
|
|
|
633
448
|
## Creating New Files
|
|
@@ -636,16 +451,16 @@ Use templates to create properly structured files:
|
|
|
636
451
|
|
|
637
452
|
\`\`\`bash
|
|
638
453
|
# Create a new module
|
|
639
|
-
npx
|
|
454
|
+
npx cx-cli create module <name>
|
|
640
455
|
|
|
641
456
|
# Create a new workflow
|
|
642
|
-
npx
|
|
457
|
+
npx cx-cli create workflow <name>
|
|
643
458
|
|
|
644
459
|
# Create from a specific template variant
|
|
645
|
-
npx
|
|
460
|
+
npx cx-cli create workflow <name> --template basic
|
|
646
461
|
|
|
647
462
|
# Create inside a feature folder (features/<name>/workflows/)
|
|
648
|
-
npx
|
|
463
|
+
npx cx-cli create workflow <name> --feature billing
|
|
649
464
|
\`\`\`
|
|
650
465
|
|
|
651
466
|
## Module Structure
|
|
@@ -890,23 +705,6 @@ function applyFieldsToForm(form, fields) {
|
|
|
890
705
|
}
|
|
891
706
|
}
|
|
892
707
|
}
|
|
893
|
-
function applyFieldsToConfiguration(layout, fields) {
|
|
894
|
-
// Configuration fields are stored under customValues, so prefix all field names
|
|
895
|
-
const configFields = fields.map(f => ({
|
|
896
|
-
component: 'field',
|
|
897
|
-
name: `customValues.${f.name}`,
|
|
898
|
-
props: {
|
|
899
|
-
type: f.type,
|
|
900
|
-
label: { 'en-US': f.label || fieldNameToLabel(f.name) },
|
|
901
|
-
...(f.required ? { required: true } : {})
|
|
902
|
-
}
|
|
903
|
-
}));
|
|
904
|
-
if (!layout.children)
|
|
905
|
-
layout.children = [];
|
|
906
|
-
layout.children.push(...configFields);
|
|
907
|
-
// Update defaultValue in configurations if present
|
|
908
|
-
// (handled separately since configurations is a top-level key)
|
|
909
|
-
}
|
|
910
708
|
function findDataGridComponents(obj) {
|
|
911
709
|
const grids = [];
|
|
912
710
|
if (!obj || typeof obj !== 'object')
|
|
@@ -1034,16 +832,9 @@ function applyCreateOptions(content, optionsArg) {
|
|
|
1034
832
|
if (!doc)
|
|
1035
833
|
throw new Error('Failed to parse template YAML for --options processing');
|
|
1036
834
|
let applied = false;
|
|
1037
|
-
const isConfiguration = Array.isArray(doc.configurations);
|
|
1038
835
|
if (doc.components && Array.isArray(doc.components)) {
|
|
1039
836
|
for (const comp of doc.components) {
|
|
1040
|
-
// Apply to
|
|
1041
|
-
if (isConfiguration && comp.layout) {
|
|
1042
|
-
applyFieldsToConfiguration(comp.layout, fields);
|
|
1043
|
-
applied = true;
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
// Apply to form components
|
|
837
|
+
// Apply to form components (configuration template)
|
|
1047
838
|
const forms = findFormComponents(comp);
|
|
1048
839
|
for (const form of forms) {
|
|
1049
840
|
applyFieldsToForm(form, fields);
|
|
@@ -1071,18 +862,6 @@ function applyCreateOptions(content, optionsArg) {
|
|
|
1071
862
|
applyFieldsToEntities(doc, fields, opts.entityName);
|
|
1072
863
|
applied = true;
|
|
1073
864
|
}
|
|
1074
|
-
// Apply defaults to configuration defaultValue
|
|
1075
|
-
if (isConfiguration && doc.configurations) {
|
|
1076
|
-
for (const config of doc.configurations) {
|
|
1077
|
-
if (!config.defaultValue)
|
|
1078
|
-
config.defaultValue = {};
|
|
1079
|
-
for (const f of fields) {
|
|
1080
|
-
if (f.default !== undefined) {
|
|
1081
|
-
config.defaultValue[f.name] = f.default;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
865
|
if (!applied) {
|
|
1087
866
|
console.warn(chalk_1.default.yellow('Warning: --options provided but no form or dataGrid component found in template'));
|
|
1088
867
|
return content;
|
|
@@ -1445,27 +1224,19 @@ function runUpdate() {
|
|
|
1445
1224
|
const { execSync } = require('child_process');
|
|
1446
1225
|
console.log(' Updating to latest version...\n');
|
|
1447
1226
|
try {
|
|
1448
|
-
execSync('npm install @cxtms/cx-schema@latest', {
|
|
1227
|
+
const output = execSync('npm install @cxtms/cx-schema@latest', {
|
|
1449
1228
|
stdio: 'inherit',
|
|
1450
1229
|
cwd: process.cwd()
|
|
1451
1230
|
});
|
|
1452
|
-
// Read installed version from the updated package
|
|
1453
|
-
const installedPkgPath = path.join(process.cwd(), 'node_modules', '@cxtms', 'cx-schema', 'package.json');
|
|
1454
|
-
let installedVersion = 'unknown';
|
|
1455
|
-
if (fs.existsSync(installedPkgPath)) {
|
|
1456
|
-
installedVersion = JSON.parse(fs.readFileSync(installedPkgPath, 'utf-8')).version;
|
|
1457
|
-
}
|
|
1458
1231
|
console.log('');
|
|
1459
|
-
console.log(chalk_1.default.green(
|
|
1232
|
+
console.log(chalk_1.default.green('✓ @cxtms/cx-schema updated successfully!'));
|
|
1233
|
+
console.log('');
|
|
1460
1234
|
}
|
|
1461
1235
|
catch (error) {
|
|
1462
1236
|
console.error(chalk_1.default.red('\nError: Failed to update @cxtms/cx-schema'));
|
|
1463
1237
|
console.error(chalk_1.default.gray(error.message));
|
|
1464
1238
|
process.exit(1);
|
|
1465
1239
|
}
|
|
1466
|
-
// Reinstall skills and update CLAUDE.md (postinstall handles schemas)
|
|
1467
|
-
runInstallSkills();
|
|
1468
|
-
runSetupClaude();
|
|
1469
1240
|
}
|
|
1470
1241
|
// ============================================================================
|
|
1471
1242
|
// Setup Claude Command
|
|
@@ -1489,23 +1260,23 @@ features/ # Feature-scoped modules and workflows
|
|
|
1489
1260
|
workflows/
|
|
1490
1261
|
\`\`\`
|
|
1491
1262
|
|
|
1492
|
-
### CLI — \`
|
|
1263
|
+
### CLI — \`cx-cli\`
|
|
1493
1264
|
|
|
1494
1265
|
**Always scaffold via CLI, never write YAML from scratch.**
|
|
1495
1266
|
|
|
1496
1267
|
| Command | Description |
|
|
1497
1268
|
|---------|-------------|
|
|
1498
|
-
| \`npx
|
|
1499
|
-
| \`npx
|
|
1500
|
-
| \`npx
|
|
1501
|
-
| \`npx
|
|
1502
|
-
| \`npx
|
|
1503
|
-
| \`npx
|
|
1504
|
-
| \`npx
|
|
1505
|
-
| \`npx
|
|
1506
|
-
| \`npx
|
|
1507
|
-
| \`npx
|
|
1508
|
-
| \`npx
|
|
1269
|
+
| \`npx cx-cli create module <name>\` | Scaffold a UI module |
|
|
1270
|
+
| \`npx cx-cli create workflow <name>\` | Scaffold a workflow |
|
|
1271
|
+
| \`npx cx-cli create module <name> --template <t>\` | Use a specific template |
|
|
1272
|
+
| \`npx cx-cli create workflow <name> --template <t>\` | Use a specific template |
|
|
1273
|
+
| \`npx cx-cli create module <name> --feature <f>\` | Place under features/<f>/modules/ |
|
|
1274
|
+
| \`npx cx-cli <file.yaml>\` | Validate a YAML file |
|
|
1275
|
+
| \`npx cx-cli <file.yaml> --verbose\` | Validate with detailed errors |
|
|
1276
|
+
| \`npx cx-cli schema <name>\` | Show JSON schema for a component or task |
|
|
1277
|
+
| \`npx cx-cli example <name>\` | Show example YAML |
|
|
1278
|
+
| \`npx cx-cli list\` | List all available schemas |
|
|
1279
|
+
| \`npx cx-cli extract <src> <comp> --to <tgt>\` | Move component between modules |
|
|
1509
1280
|
|
|
1510
1281
|
**Module templates:** \`default\`, \`form\`, \`grid\`, \`select\`, \`configuration\`
|
|
1511
1282
|
**Workflow templates:** \`basic\`, \`entity-trigger\`, \`document\`, \`scheduled\`, \`utility\`, \`webhook\`, \`public-api\`, \`mcp-tool\`, \`ftp-tracking\`, \`ftp-edi\`, \`api-tracking\`
|
|
@@ -1520,10 +1291,10 @@ features/ # Feature-scoped modules and workflows
|
|
|
1520
1291
|
|
|
1521
1292
|
### Workflow: Scaffold → Customize → Validate
|
|
1522
1293
|
|
|
1523
|
-
1. **Scaffold** — \`npx
|
|
1294
|
+
1. **Scaffold** — \`npx cx-cli create module|workflow <name> --template <t>\`
|
|
1524
1295
|
2. **Read** the generated file
|
|
1525
1296
|
3. **Customize** for the use case
|
|
1526
|
-
4. **Validate** — \`npx
|
|
1297
|
+
4. **Validate** — \`npx cx-cli <file.yaml>\` — run after every change, fix all errors
|
|
1527
1298
|
${CX_CLAUDE_MARKER}`;
|
|
1528
1299
|
}
|
|
1529
1300
|
function runSetupClaude() {
|
|
@@ -1558,1960 +1329,149 @@ function runSetupClaude() {
|
|
|
1558
1329
|
console.log('');
|
|
1559
1330
|
}
|
|
1560
1331
|
// ============================================================================
|
|
1561
|
-
//
|
|
1332
|
+
// Extract Command
|
|
1562
1333
|
// ============================================================================
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
function getSessionFilePath() {
|
|
1570
|
-
return path.join(getSessionDir(), '.session.json');
|
|
1571
|
-
}
|
|
1572
|
-
function readSessionFile() {
|
|
1573
|
-
const filePath = getSessionFilePath();
|
|
1574
|
-
if (!fs.existsSync(filePath))
|
|
1575
|
-
return null;
|
|
1576
|
-
try {
|
|
1577
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1334
|
+
function runExtract(sourceFile, componentName, targetFile, copy) {
|
|
1335
|
+
// Validate args
|
|
1336
|
+
if (!sourceFile || !componentName || !targetFile) {
|
|
1337
|
+
console.error(chalk_1.default.red('Error: Missing required arguments'));
|
|
1338
|
+
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file> [--copy]`));
|
|
1339
|
+
process.exit(2);
|
|
1578
1340
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1341
|
+
// Check source exists
|
|
1342
|
+
if (!fs.existsSync(sourceFile)) {
|
|
1343
|
+
console.error(chalk_1.default.red(`Error: Source file not found: ${sourceFile}`));
|
|
1344
|
+
process.exit(2);
|
|
1581
1345
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1346
|
+
// Read and parse source (Document API preserves comments)
|
|
1347
|
+
const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
|
|
1348
|
+
const srcDoc = yaml_1.default.parseDocument(sourceContent);
|
|
1349
|
+
const sourceJS = srcDoc.toJS();
|
|
1350
|
+
if (!sourceJS || !Array.isArray(sourceJS.components)) {
|
|
1351
|
+
console.error(chalk_1.default.red(`Error: Source file is not a valid module (missing components array): ${sourceFile}`));
|
|
1352
|
+
process.exit(2);
|
|
1587
1353
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
fs.unlinkSync(filePath);
|
|
1354
|
+
// Get the AST components sequence
|
|
1355
|
+
const srcComponents = srcDoc.get('components', true);
|
|
1356
|
+
if (!(0, yaml_1.isSeq)(srcComponents)) {
|
|
1357
|
+
console.error(chalk_1.default.red(`Error: Source components is not a sequence: ${sourceFile}`));
|
|
1358
|
+
process.exit(2);
|
|
1594
1359
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1599
|
-
function generateCodeChallenge(verifier) {
|
|
1600
|
-
return crypto.createHash('sha256').update(verifier).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
1601
|
-
}
|
|
1602
|
-
function httpsPost(url, body, contentType) {
|
|
1603
|
-
return new Promise((resolve, reject) => {
|
|
1604
|
-
const parsed = new URL(url);
|
|
1605
|
-
const isHttps = parsed.protocol === 'https:';
|
|
1606
|
-
const lib = isHttps ? https : http;
|
|
1607
|
-
const req = lib.request({
|
|
1608
|
-
hostname: parsed.hostname,
|
|
1609
|
-
port: parsed.port || (isHttps ? 443 : 80),
|
|
1610
|
-
path: parsed.pathname + parsed.search,
|
|
1611
|
-
method: 'POST',
|
|
1612
|
-
headers: {
|
|
1613
|
-
'Content-Type': contentType,
|
|
1614
|
-
'Content-Length': Buffer.byteLength(body),
|
|
1615
|
-
},
|
|
1616
|
-
}, (res) => {
|
|
1617
|
-
let data = '';
|
|
1618
|
-
res.on('data', (chunk) => data += chunk);
|
|
1619
|
-
res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
|
|
1620
|
-
});
|
|
1621
|
-
req.on('error', reject);
|
|
1622
|
-
req.write(body);
|
|
1623
|
-
req.end();
|
|
1360
|
+
// Find component by exact name match
|
|
1361
|
+
const compIndex = srcComponents.items.findIndex((item) => {
|
|
1362
|
+
return (0, yaml_1.isMap)(item) && item.get('name') === componentName;
|
|
1624
1363
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
function startCallbackServer() {
|
|
1634
|
-
return new Promise((resolve, reject) => {
|
|
1635
|
-
const server = http.createServer((req, res) => {
|
|
1636
|
-
const reqUrl = new URL(req.url || '/', `http://127.0.0.1:${AUTH_CALLBACK_PORT}`);
|
|
1637
|
-
if (reqUrl.pathname === '/callback') {
|
|
1638
|
-
const code = reqUrl.searchParams.get('code');
|
|
1639
|
-
const error = reqUrl.searchParams.get('error');
|
|
1640
|
-
if (error) {
|
|
1641
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1642
|
-
res.end('<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>');
|
|
1643
|
-
reject(new Error(`OAuth error: ${error} - ${reqUrl.searchParams.get('error_description') || ''}`));
|
|
1644
|
-
server.close();
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
if (code) {
|
|
1648
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1649
|
-
res.end('<html><body><h2>Login successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
|
|
1650
|
-
resolve({ code, close: () => server.close() });
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
res.writeHead(404);
|
|
1655
|
-
res.end();
|
|
1656
|
-
});
|
|
1657
|
-
server.on('error', (err) => {
|
|
1658
|
-
if (err.code === 'EADDRINUSE') {
|
|
1659
|
-
reject(new Error(`Port ${AUTH_CALLBACK_PORT} is already in use. Close the process using it and try again.`));
|
|
1660
|
-
}
|
|
1661
|
-
else {
|
|
1662
|
-
reject(err);
|
|
1364
|
+
if (compIndex === -1) {
|
|
1365
|
+
const available = sourceJS.components.map((c) => c.name).filter(Boolean);
|
|
1366
|
+
console.error(chalk_1.default.red(`Error: Component not found: ${componentName}`));
|
|
1367
|
+
if (available.length > 0) {
|
|
1368
|
+
console.error(chalk_1.default.gray('Available components:'));
|
|
1369
|
+
for (const name of available) {
|
|
1370
|
+
console.error(chalk_1.default.gray(` - ${name}`));
|
|
1663
1371
|
}
|
|
1664
|
-
}
|
|
1665
|
-
server.listen(AUTH_CALLBACK_PORT, '127.0.0.1');
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
async function registerOAuthClient(domain) {
|
|
1669
|
-
const res = await httpsPost(`${domain}/connect/register`, JSON.stringify({
|
|
1670
|
-
client_name: `cxtms-${crypto.randomBytes(4).toString('hex')}`,
|
|
1671
|
-
redirect_uris: [`http://localhost:${AUTH_CALLBACK_PORT}/callback`],
|
|
1672
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
1673
|
-
response_types: ['code'],
|
|
1674
|
-
token_endpoint_auth_method: 'none',
|
|
1675
|
-
}), 'application/json');
|
|
1676
|
-
if (res.statusCode !== 200 && res.statusCode !== 201) {
|
|
1677
|
-
throw new Error(`Client registration failed (${res.statusCode}): ${res.body}`);
|
|
1678
|
-
}
|
|
1679
|
-
const data = JSON.parse(res.body);
|
|
1680
|
-
if (!data.client_id) {
|
|
1681
|
-
throw new Error('Client registration response missing client_id');
|
|
1682
|
-
}
|
|
1683
|
-
return data.client_id;
|
|
1684
|
-
}
|
|
1685
|
-
async function exchangeCodeForTokens(domain, clientId, code, codeVerifier) {
|
|
1686
|
-
const body = new URLSearchParams({
|
|
1687
|
-
grant_type: 'authorization_code',
|
|
1688
|
-
client_id: clientId,
|
|
1689
|
-
code,
|
|
1690
|
-
redirect_uri: `http://localhost:${AUTH_CALLBACK_PORT}/callback`,
|
|
1691
|
-
code_verifier: codeVerifier,
|
|
1692
|
-
}).toString();
|
|
1693
|
-
const res = await httpsPost(`${domain}/connect/token`, body, 'application/x-www-form-urlencoded');
|
|
1694
|
-
if (res.statusCode !== 200) {
|
|
1695
|
-
throw new Error(`Token exchange failed (${res.statusCode}): ${res.body}`);
|
|
1696
|
-
}
|
|
1697
|
-
const data = JSON.parse(res.body);
|
|
1698
|
-
return {
|
|
1699
|
-
domain,
|
|
1700
|
-
client_id: clientId,
|
|
1701
|
-
access_token: data.access_token,
|
|
1702
|
-
refresh_token: data.refresh_token,
|
|
1703
|
-
expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
async function revokeToken(domain, clientId, token) {
|
|
1707
|
-
try {
|
|
1708
|
-
await httpsPost(`${domain}/connect/revoke`, new URLSearchParams({ client_id: clientId, token }).toString(), 'application/x-www-form-urlencoded');
|
|
1709
|
-
}
|
|
1710
|
-
catch {
|
|
1711
|
-
// Revocation failures are non-fatal
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
async function refreshTokens(stored) {
|
|
1715
|
-
const body = new URLSearchParams({
|
|
1716
|
-
grant_type: 'refresh_token',
|
|
1717
|
-
client_id: stored.client_id,
|
|
1718
|
-
refresh_token: stored.refresh_token,
|
|
1719
|
-
}).toString();
|
|
1720
|
-
const res = await httpsPost(`${stored.domain}/connect/token`, body, 'application/x-www-form-urlencoded');
|
|
1721
|
-
if (res.statusCode !== 200) {
|
|
1722
|
-
throw new Error(`Token refresh failed (${res.statusCode}): ${res.body}`);
|
|
1723
|
-
}
|
|
1724
|
-
const data = JSON.parse(res.body);
|
|
1725
|
-
const updated = {
|
|
1726
|
-
...stored,
|
|
1727
|
-
access_token: data.access_token,
|
|
1728
|
-
refresh_token: data.refresh_token || stored.refresh_token,
|
|
1729
|
-
expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
|
|
1730
|
-
};
|
|
1731
|
-
writeSessionFile(updated);
|
|
1732
|
-
return updated;
|
|
1733
|
-
}
|
|
1734
|
-
async function runLogin(domain) {
|
|
1735
|
-
// Normalize URL
|
|
1736
|
-
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
|
1737
|
-
domain = `https://${domain}`;
|
|
1738
|
-
}
|
|
1739
|
-
domain = domain.replace(/\/+$/, '');
|
|
1740
|
-
try {
|
|
1741
|
-
new URL(domain);
|
|
1742
|
-
}
|
|
1743
|
-
catch {
|
|
1744
|
-
console.error(chalk_1.default.red('Error: Invalid URL'));
|
|
1372
|
+
}
|
|
1745
1373
|
process.exit(2);
|
|
1746
1374
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const authUrl = `${domain}/connect/authorize?` + new URLSearchParams({
|
|
1759
|
-
client_id: clientId,
|
|
1760
|
-
redirect_uri: `http://localhost:${AUTH_CALLBACK_PORT}/callback`,
|
|
1761
|
-
response_type: 'code',
|
|
1762
|
-
scope: 'openid offline_access TMS.ApiAPI',
|
|
1763
|
-
code_challenge: codeChallenge,
|
|
1764
|
-
code_challenge_method: 'S256',
|
|
1765
|
-
}).toString();
|
|
1766
|
-
console.log(chalk_1.default.gray(' Opening browser for login...'));
|
|
1767
|
-
openBrowser(authUrl);
|
|
1768
|
-
console.log(chalk_1.default.gray(` Waiting for login (timeout: 2 min)...`));
|
|
1769
|
-
// Step 5: Wait for callback with timeout
|
|
1770
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Login timed out after 2 minutes. Please try again.')), AUTH_TIMEOUT_MS));
|
|
1771
|
-
const { code, close } = await Promise.race([callbackPromise, timeoutPromise]);
|
|
1772
|
-
// Step 6: Exchange code for tokens
|
|
1773
|
-
console.log(chalk_1.default.gray(' Exchanging authorization code...'));
|
|
1774
|
-
const tokens = await exchangeCodeForTokens(domain, clientId, code, codeVerifier);
|
|
1775
|
-
// Step 7: Store session locally
|
|
1776
|
-
writeSessionFile(tokens);
|
|
1777
|
-
close();
|
|
1778
|
-
console.log(chalk_1.default.green(` ✓ Logged in to ${new URL(domain).hostname}`));
|
|
1779
|
-
console.log(chalk_1.default.gray(` Session stored at: ${getSessionFilePath()}\n`));
|
|
1780
|
-
}
|
|
1781
|
-
async function runLogout(_domain) {
|
|
1782
|
-
const session = readSessionFile();
|
|
1783
|
-
if (!session) {
|
|
1784
|
-
console.log(chalk_1.default.gray('\n No active session in this project.\n'));
|
|
1785
|
-
console.log(chalk_1.default.gray(` Login first: ${PROGRAM_NAME} login <url>\n`));
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
console.log(chalk_1.default.bold.cyan('\n CX CLI Logout\n'));
|
|
1789
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(session.domain).hostname}`));
|
|
1790
|
-
// Revoke tokens (non-fatal)
|
|
1791
|
-
if (session.client_id && session.refresh_token) {
|
|
1792
|
-
console.log(chalk_1.default.gray(' Revoking tokens...'));
|
|
1793
|
-
await revokeToken(session.domain, session.client_id, session.access_token);
|
|
1794
|
-
await revokeToken(session.domain, session.client_id, session.refresh_token);
|
|
1795
|
-
}
|
|
1796
|
-
// Delete local session file
|
|
1797
|
-
deleteSessionFile();
|
|
1798
|
-
console.log(chalk_1.default.green(` ✓ Logged out from ${new URL(session.domain).hostname}\n`));
|
|
1799
|
-
}
|
|
1800
|
-
// ============================================================================
|
|
1801
|
-
// AppModule Commands
|
|
1802
|
-
// ============================================================================
|
|
1803
|
-
async function graphqlRequest(domain, token, query, variables) {
|
|
1804
|
-
const body = JSON.stringify({ query, variables });
|
|
1805
|
-
let res = await graphqlPostWithAuth(domain, token, body);
|
|
1806
|
-
if (res.statusCode === 401) {
|
|
1807
|
-
// PAT tokens have no refresh — fail immediately
|
|
1808
|
-
if (process.env.CXTMS_AUTH)
|
|
1809
|
-
throw new Error('PAT token unauthorized (401). Check your CXTMS_AUTH token.');
|
|
1810
|
-
// Try refresh for OAuth sessions
|
|
1811
|
-
const stored = readSessionFile();
|
|
1812
|
-
if (!stored)
|
|
1813
|
-
throw new Error('Session expired. Run `cxtms login <url>` again.');
|
|
1814
|
-
try {
|
|
1815
|
-
const refreshed = await refreshTokens(stored);
|
|
1816
|
-
res = await graphqlPostWithAuth(domain, refreshed.access_token, body);
|
|
1817
|
-
}
|
|
1818
|
-
catch {
|
|
1819
|
-
throw new Error('Session expired. Run `cxtms login <url>` again.');
|
|
1375
|
+
// Get the component AST node (clone for copy, take for move)
|
|
1376
|
+
const componentNode = copy
|
|
1377
|
+
? srcDoc.createNode(sourceJS.components[compIndex])
|
|
1378
|
+
: srcComponents.items[compIndex];
|
|
1379
|
+
// Capture comment: if this is the first item, the comment lives on the parent seq
|
|
1380
|
+
let componentComment;
|
|
1381
|
+
if (compIndex === 0 && srcComponents.commentBefore) {
|
|
1382
|
+
componentComment = srcComponents.commentBefore;
|
|
1383
|
+
if (!copy) {
|
|
1384
|
+
// Transfer the comment away from the source seq (it belongs to the extracted component)
|
|
1385
|
+
srcComponents.commentBefore = undefined;
|
|
1820
1386
|
}
|
|
1821
1387
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
try {
|
|
1825
|
-
json = JSON.parse(res.body);
|
|
1388
|
+
else {
|
|
1389
|
+
componentComment = componentNode.commentBefore;
|
|
1826
1390
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const parts = [e.message];
|
|
1836
|
-
const ext = e.extensions?.message;
|
|
1837
|
-
if (ext && ext !== e.message)
|
|
1838
|
-
parts.push(ext);
|
|
1839
|
-
if (e.path)
|
|
1840
|
-
parts.push(`path: ${e.path.join('.')}`);
|
|
1841
|
-
return parts.join(' — ');
|
|
1391
|
+
// Find matching routes (by index in AST)
|
|
1392
|
+
const srcRoutes = srcDoc.get('routes', true);
|
|
1393
|
+
const matchedRouteIndices = [];
|
|
1394
|
+
if ((0, yaml_1.isSeq)(srcRoutes)) {
|
|
1395
|
+
srcRoutes.items.forEach((item, idx) => {
|
|
1396
|
+
if ((0, yaml_1.isMap)(item) && item.get('component') === componentName) {
|
|
1397
|
+
matchedRouteIndices.push(idx);
|
|
1398
|
+
}
|
|
1842
1399
|
});
|
|
1843
|
-
throw new Error(`GraphQL error: ${messages.join('; ')}`);
|
|
1844
1400
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
return new Promise((resolve, reject) => {
|
|
1852
|
-
const url = `${domain}/api/graphql`;
|
|
1853
|
-
const parsed = new URL(url);
|
|
1854
|
-
const isHttps = parsed.protocol === 'https:';
|
|
1855
|
-
const lib = isHttps ? https : http;
|
|
1856
|
-
const req = lib.request({
|
|
1857
|
-
hostname: parsed.hostname,
|
|
1858
|
-
port: parsed.port || (isHttps ? 443 : 80),
|
|
1859
|
-
path: parsed.pathname + parsed.search,
|
|
1860
|
-
method: 'POST',
|
|
1861
|
-
headers: {
|
|
1862
|
-
'Content-Type': 'application/json',
|
|
1863
|
-
'Content-Length': Buffer.byteLength(body),
|
|
1864
|
-
'Authorization': `Bearer ${token}`,
|
|
1865
|
-
},
|
|
1866
|
-
}, (res) => {
|
|
1867
|
-
let data = '';
|
|
1868
|
-
res.on('data', (chunk) => data += chunk);
|
|
1869
|
-
res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
|
|
1870
|
-
});
|
|
1871
|
-
req.on('error', reject);
|
|
1872
|
-
req.write(body);
|
|
1873
|
-
req.end();
|
|
1401
|
+
// Collect route AST nodes (clone for copy, reference for move)
|
|
1402
|
+
const routeNodes = matchedRouteIndices.map(idx => {
|
|
1403
|
+
if (copy) {
|
|
1404
|
+
return srcDoc.createNode(sourceJS.routes[idx]);
|
|
1405
|
+
}
|
|
1406
|
+
return srcRoutes.items[idx];
|
|
1874
1407
|
});
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
let domain = serverDomain;
|
|
1885
|
-
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
|
1886
|
-
domain = `https://${domain}`;
|
|
1887
|
-
}
|
|
1888
|
-
return domain.replace(/\/+$/, '');
|
|
1889
|
-
}
|
|
1890
|
-
function resolveSession() {
|
|
1891
|
-
// 0. Check for PAT token in env (CXTMS_AUTH) — skips OAuth entirely
|
|
1892
|
-
const patToken = process.env.CXTMS_AUTH;
|
|
1893
|
-
if (patToken) {
|
|
1894
|
-
const domain = process.env.CXTMS_SERVER ? process.env.CXTMS_SERVER.replace(/\/+$/, '') : resolveDomainFromAppYaml();
|
|
1895
|
-
if (!domain) {
|
|
1896
|
-
console.error(chalk_1.default.red('CXTMS_AUTH is set but no server domain found.'));
|
|
1897
|
-
console.error(chalk_1.default.gray('Add `server` to app.yaml or set CXTMS_SERVER in .env'));
|
|
1408
|
+
// Load or create target document
|
|
1409
|
+
let tgtDoc;
|
|
1410
|
+
let targetCreated = false;
|
|
1411
|
+
if (fs.existsSync(targetFile)) {
|
|
1412
|
+
const targetContent = fs.readFileSync(targetFile, 'utf-8');
|
|
1413
|
+
tgtDoc = yaml_1.default.parseDocument(targetContent);
|
|
1414
|
+
const targetJS = tgtDoc.toJS();
|
|
1415
|
+
if (!targetJS || !Array.isArray(targetJS.components)) {
|
|
1416
|
+
console.error(chalk_1.default.red(`Error: Target file is not a valid module (missing components array): ${targetFile}`));
|
|
1898
1417
|
process.exit(2);
|
|
1899
1418
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
expires_at: 0,
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
// 1. Check local .cxtms/.session.json
|
|
1909
|
-
const session = readSessionFile();
|
|
1910
|
-
if (session)
|
|
1911
|
-
return session;
|
|
1912
|
-
// 2. Not logged in
|
|
1913
|
-
console.error(chalk_1.default.red('Not logged in. Run `cxtms login <url>` first.'));
|
|
1914
|
-
process.exit(2);
|
|
1915
|
-
}
|
|
1916
|
-
async function resolveOrgId(domain, token, override) {
|
|
1917
|
-
// 1. Explicit override
|
|
1918
|
-
if (override !== undefined)
|
|
1919
|
-
return override;
|
|
1920
|
-
// 2. Cached in session file
|
|
1921
|
-
const stored = readSessionFile();
|
|
1922
|
-
if (stored?.organization_id)
|
|
1923
|
-
return stored.organization_id;
|
|
1924
|
-
// 3. Query server
|
|
1925
|
-
const data = await graphqlRequest(domain, token, `
|
|
1926
|
-
query { organizations(take: 100) { items { organizationId companyName } } }
|
|
1927
|
-
`, {});
|
|
1928
|
-
const orgs = data?.organizations?.items;
|
|
1929
|
-
if (!orgs || orgs.length === 0) {
|
|
1930
|
-
throw new Error('No organizations found for this account.');
|
|
1931
|
-
}
|
|
1932
|
-
if (orgs.length === 1) {
|
|
1933
|
-
const orgId = orgs[0].organizationId;
|
|
1934
|
-
// Cache it
|
|
1935
|
-
if (stored) {
|
|
1936
|
-
stored.organization_id = orgId;
|
|
1937
|
-
writeSessionFile(stored);
|
|
1419
|
+
// Check for duplicate component name
|
|
1420
|
+
const duplicate = targetJS.components.find((c) => c.name === componentName);
|
|
1421
|
+
if (duplicate) {
|
|
1422
|
+
console.error(chalk_1.default.red(`Error: Target already contains a component named "${componentName}"`));
|
|
1423
|
+
process.exit(2);
|
|
1938
1424
|
}
|
|
1939
|
-
return orgId;
|
|
1940
|
-
}
|
|
1941
|
-
// Multiple orgs — list and exit
|
|
1942
|
-
console.error(chalk_1.default.yellow('\n Multiple organizations found:\n'));
|
|
1943
|
-
for (const org of orgs) {
|
|
1944
|
-
console.error(chalk_1.default.white(` ${org.organizationId} ${org.companyName}`));
|
|
1945
|
-
}
|
|
1946
|
-
console.error(chalk_1.default.gray(`\n Run \`cxtms orgs select\` to choose, or pass --org <id>.\n`));
|
|
1947
|
-
process.exit(2);
|
|
1948
|
-
}
|
|
1949
|
-
async function runAppModuleDeploy(file, orgOverride) {
|
|
1950
|
-
if (!file) {
|
|
1951
|
-
console.error(chalk_1.default.red('Error: File path required'));
|
|
1952
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule deploy <file.yaml> [--org <id>]`));
|
|
1953
|
-
process.exit(2);
|
|
1954
|
-
}
|
|
1955
|
-
if (!fs.existsSync(file)) {
|
|
1956
|
-
console.error(chalk_1.default.red(`Error: File not found: ${file}`));
|
|
1957
|
-
process.exit(2);
|
|
1958
1425
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1426
|
+
else {
|
|
1427
|
+
// Create new module scaffold
|
|
1428
|
+
const baseName = path.basename(targetFile, path.extname(targetFile));
|
|
1429
|
+
const moduleName = baseName
|
|
1430
|
+
.split('-')
|
|
1431
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1432
|
+
.join('');
|
|
1433
|
+
const sourceModule = typeof sourceJS.module === 'object' ? sourceJS.module : null;
|
|
1434
|
+
const displayName = moduleName.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
1435
|
+
const moduleObj = {
|
|
1436
|
+
name: moduleName,
|
|
1437
|
+
appModuleId: generateUUID(),
|
|
1438
|
+
displayName: { 'en-US': displayName },
|
|
1439
|
+
description: { 'en-US': `${displayName} module` },
|
|
1440
|
+
application: sourceModule?.application || sourceJS.application || 'cx',
|
|
1441
|
+
};
|
|
1442
|
+
// In copy mode, set priority higher than source
|
|
1443
|
+
if (copy) {
|
|
1444
|
+
const sourcePriority = sourceModule?.priority;
|
|
1445
|
+
moduleObj.priority = (0, extractUtils_1.computeExtractPriority)(sourcePriority);
|
|
1446
|
+
}
|
|
1447
|
+
// Parse from string so the document has proper AST context for comment preservation
|
|
1448
|
+
const scaffoldStr = yaml_1.default.stringify({
|
|
1449
|
+
module: moduleObj,
|
|
1450
|
+
entities: [],
|
|
1451
|
+
permissions: [],
|
|
1452
|
+
components: [],
|
|
1453
|
+
routes: []
|
|
1454
|
+
}, { indent: 2, lineWidth: 0, singleQuote: false });
|
|
1455
|
+
tgtDoc = yaml_1.default.parseDocument(scaffoldStr);
|
|
1456
|
+
targetCreated = true;
|
|
1970
1457
|
}
|
|
1971
|
-
//
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
// Check if module exists
|
|
1984
|
-
const checkData = await graphqlRequest(domain, token, `
|
|
1985
|
-
query ($organizationId: Int!, $appModuleId: UUID!) {
|
|
1986
|
-
appModule(organizationId: $organizationId, appModuleId: $appModuleId) {
|
|
1987
|
-
appModuleId
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
`, { organizationId: orgId, appModuleId });
|
|
1991
|
-
if (checkData?.appModule) {
|
|
1992
|
-
// Update
|
|
1993
|
-
console.log(chalk_1.default.gray(' Updating existing module...'));
|
|
1994
|
-
const updateValues = { appModuleYamlDocument: yamlContent };
|
|
1995
|
-
if (appManifestId)
|
|
1996
|
-
updateValues.appManifestId = appManifestId;
|
|
1997
|
-
const result = await graphqlRequest(domain, token, `
|
|
1998
|
-
mutation ($input: UpdateAppModuleInput!) {
|
|
1999
|
-
updateAppModule(input: $input) {
|
|
2000
|
-
appModule { appModuleId name }
|
|
1458
|
+
// Add component to target (ensure block style so comments are preserved)
|
|
1459
|
+
const tgtComponents = tgtDoc.get('components', true);
|
|
1460
|
+
if ((0, yaml_1.isSeq)(tgtComponents)) {
|
|
1461
|
+
tgtComponents.flow = false;
|
|
1462
|
+
// Apply the captured comment: if it's the first item in target, set on seq; otherwise on node
|
|
1463
|
+
if (componentComment) {
|
|
1464
|
+
if (tgtComponents.items.length === 0) {
|
|
1465
|
+
tgtComponents.commentBefore = componentComment;
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
componentNode.commentBefore = componentComment;
|
|
1469
|
+
}
|
|
2001
1470
|
}
|
|
2002
|
-
|
|
2003
|
-
`, {
|
|
2004
|
-
input: {
|
|
2005
|
-
organizationId: orgId,
|
|
2006
|
-
appModuleId,
|
|
2007
|
-
values: updateValues,
|
|
2008
|
-
},
|
|
2009
|
-
});
|
|
2010
|
-
const mod = result?.updateAppModule?.appModule;
|
|
2011
|
-
console.log(chalk_1.default.green(` ✓ Updated: ${mod?.name || appModuleId}\n`));
|
|
1471
|
+
tgtComponents.items.push(componentNode);
|
|
2012
1472
|
}
|
|
2013
1473
|
else {
|
|
2014
|
-
|
|
2015
|
-
console.log(chalk_1.default.gray(' Creating new module...'));
|
|
2016
|
-
const values = { appModuleYamlDocument: yamlContent };
|
|
2017
|
-
if (appManifestId)
|
|
2018
|
-
values.appManifestId = appManifestId;
|
|
2019
|
-
const result = await graphqlRequest(domain, token, `
|
|
2020
|
-
mutation ($input: CreateAppModuleInput!) {
|
|
2021
|
-
createAppModule(input: $input) {
|
|
2022
|
-
appModule { appModuleId name }
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
`, {
|
|
2026
|
-
input: {
|
|
2027
|
-
organizationId: orgId,
|
|
2028
|
-
values,
|
|
2029
|
-
},
|
|
2030
|
-
});
|
|
2031
|
-
const mod = result?.createAppModule?.appModule;
|
|
2032
|
-
console.log(chalk_1.default.green(` ✓ Created: ${mod?.name || appModuleId}\n`));
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
async function runAppModuleUndeploy(uuid, orgOverride) {
|
|
2036
|
-
if (!uuid) {
|
|
2037
|
-
console.error(chalk_1.default.red('Error: AppModule ID required'));
|
|
2038
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule undeploy <appModuleId> [--org <id>]`));
|
|
2039
|
-
process.exit(2);
|
|
2040
|
-
}
|
|
2041
|
-
const session = resolveSession();
|
|
2042
|
-
const domain = session.domain;
|
|
2043
|
-
const token = session.access_token;
|
|
2044
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2045
|
-
console.log(chalk_1.default.bold.cyan('\n AppModule Undeploy\n'));
|
|
2046
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2047
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2048
|
-
console.log(chalk_1.default.gray(` Module: ${uuid}`));
|
|
2049
|
-
console.log('');
|
|
2050
|
-
await graphqlRequest(domain, token, `
|
|
2051
|
-
mutation ($input: DeleteAppModuleInput!) {
|
|
2052
|
-
deleteAppModule(input: $input) {
|
|
2053
|
-
deleteResult { __typename }
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
`, {
|
|
2057
|
-
input: {
|
|
2058
|
-
organizationId: orgId,
|
|
2059
|
-
appModuleId: uuid,
|
|
2060
|
-
},
|
|
2061
|
-
});
|
|
2062
|
-
console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
|
|
2063
|
-
}
|
|
2064
|
-
async function runOrgsList() {
|
|
2065
|
-
const session = resolveSession();
|
|
2066
|
-
const domain = session.domain;
|
|
2067
|
-
const token = session.access_token;
|
|
2068
|
-
const data = await graphqlRequest(domain, token, `
|
|
2069
|
-
query { organizations(take: 100) { items { organizationId companyName } } }
|
|
2070
|
-
`, {});
|
|
2071
|
-
const orgs = data?.organizations?.items;
|
|
2072
|
-
if (!orgs || orgs.length === 0) {
|
|
2073
|
-
console.log(chalk_1.default.gray('\n No organizations found.\n'));
|
|
2074
|
-
return;
|
|
2075
|
-
}
|
|
2076
|
-
console.log(chalk_1.default.bold.cyan('\n Organizations\n'));
|
|
2077
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}\n`));
|
|
2078
|
-
for (const org of orgs) {
|
|
2079
|
-
const current = session.organization_id === org.organizationId;
|
|
2080
|
-
const marker = current ? chalk_1.default.green(' ← current') : '';
|
|
2081
|
-
console.log(chalk_1.default.white(` ${org.organizationId} ${org.companyName}${marker}`));
|
|
2082
|
-
}
|
|
2083
|
-
console.log('');
|
|
2084
|
-
}
|
|
2085
|
-
async function runOrgsUse(orgIdStr) {
|
|
2086
|
-
if (!orgIdStr) {
|
|
2087
|
-
// Show current context
|
|
2088
|
-
const session = resolveSession();
|
|
2089
|
-
const domain = session.domain;
|
|
2090
|
-
console.log(chalk_1.default.bold.cyan('\n Current Context\n'));
|
|
2091
|
-
console.log(chalk_1.default.white(` Server: ${new URL(domain).hostname}`));
|
|
2092
|
-
if (session.organization_id) {
|
|
2093
|
-
console.log(chalk_1.default.white(` Org: ${session.organization_id}`));
|
|
2094
|
-
}
|
|
2095
|
-
else {
|
|
2096
|
-
console.log(chalk_1.default.gray(` Org: (not set)`));
|
|
2097
|
-
}
|
|
2098
|
-
const appYamlPath = path.join(process.cwd(), 'app.yaml');
|
|
2099
|
-
if (fs.existsSync(appYamlPath)) {
|
|
2100
|
-
const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
|
|
2101
|
-
if (appYaml?.id) {
|
|
2102
|
-
console.log(chalk_1.default.white(` App: ${appYaml.id} ${chalk_1.default.gray('(from app.yaml)')}`));
|
|
2103
|
-
}
|
|
2104
|
-
else {
|
|
2105
|
-
console.log(chalk_1.default.gray(` App: (not set)`));
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
else {
|
|
2109
|
-
console.log(chalk_1.default.gray(` App: (not set)`));
|
|
2110
|
-
}
|
|
2111
|
-
console.log('');
|
|
2112
|
-
return;
|
|
2113
|
-
}
|
|
2114
|
-
const orgId = parseInt(orgIdStr, 10);
|
|
2115
|
-
if (isNaN(orgId)) {
|
|
2116
|
-
console.error(chalk_1.default.red(`Invalid organization ID: ${orgIdStr}. Must be a number.`));
|
|
2117
|
-
process.exit(2);
|
|
2118
|
-
}
|
|
2119
|
-
const session = resolveSession();
|
|
2120
|
-
const domain = session.domain;
|
|
2121
|
-
const token = session.access_token;
|
|
2122
|
-
// Validate the org exists
|
|
2123
|
-
const data = await graphqlRequest(domain, token, `
|
|
2124
|
-
query { organizations(take: 100) { items { organizationId companyName } } }
|
|
2125
|
-
`, {});
|
|
2126
|
-
const orgs = data?.organizations?.items;
|
|
2127
|
-
const match = orgs?.find((o) => o.organizationId === orgId);
|
|
2128
|
-
if (!match) {
|
|
2129
|
-
console.error(chalk_1.default.red(`Organization ${orgId} not found.`));
|
|
2130
|
-
if (orgs?.length) {
|
|
2131
|
-
console.error(chalk_1.default.gray('\n Available organizations:'));
|
|
2132
|
-
for (const org of orgs) {
|
|
2133
|
-
console.error(chalk_1.default.white(` ${org.organizationId} ${org.companyName}`));
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
console.error('');
|
|
2137
|
-
process.exit(2);
|
|
2138
|
-
}
|
|
2139
|
-
// Save to session file
|
|
2140
|
-
session.organization_id = orgId;
|
|
2141
|
-
writeSessionFile(session);
|
|
2142
|
-
console.log(chalk_1.default.green(`\n ✓ Context set to: ${match.companyName} (${orgId})\n`));
|
|
2143
|
-
}
|
|
2144
|
-
async function runOrgsSelect() {
|
|
2145
|
-
const session = resolveSession();
|
|
2146
|
-
const domain = session.domain;
|
|
2147
|
-
const token = session.access_token;
|
|
2148
|
-
const data = await graphqlRequest(domain, token, `
|
|
2149
|
-
query { organizations(take: 100) { items { organizationId companyName } } }
|
|
2150
|
-
`, {});
|
|
2151
|
-
const orgs = data?.organizations?.items;
|
|
2152
|
-
if (!orgs || orgs.length === 0) {
|
|
2153
|
-
console.log(chalk_1.default.gray('\n No organizations found.\n'));
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
console.log(chalk_1.default.bold.cyan('\n Select Organization\n'));
|
|
2157
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}\n`));
|
|
2158
|
-
for (let i = 0; i < orgs.length; i++) {
|
|
2159
|
-
const org = orgs[i];
|
|
2160
|
-
const current = session.organization_id === org.organizationId;
|
|
2161
|
-
const marker = current ? chalk_1.default.green(' ← current') : '';
|
|
2162
|
-
console.log(chalk_1.default.white(` ${i + 1}) ${org.organizationId} ${org.companyName}${marker}`));
|
|
2163
|
-
}
|
|
2164
|
-
console.log('');
|
|
2165
|
-
const readline = require('readline');
|
|
2166
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2167
|
-
const answer = await new Promise((resolve) => {
|
|
2168
|
-
rl.question(chalk_1.default.yellow(' Enter number: '), (ans) => {
|
|
2169
|
-
rl.close();
|
|
2170
|
-
resolve(ans.trim());
|
|
2171
|
-
});
|
|
2172
|
-
});
|
|
2173
|
-
const idx = parseInt(answer, 10) - 1;
|
|
2174
|
-
if (isNaN(idx) || idx < 0 || idx >= orgs.length) {
|
|
2175
|
-
console.error(chalk_1.default.red('\n Invalid selection.\n'));
|
|
2176
|
-
process.exit(2);
|
|
2177
|
-
}
|
|
2178
|
-
const selected = orgs[idx];
|
|
2179
|
-
session.organization_id = selected.organizationId;
|
|
2180
|
-
writeSessionFile(session);
|
|
2181
|
-
console.log(chalk_1.default.green(`\n ✓ Context set to: ${selected.companyName} (${selected.organizationId})\n`));
|
|
2182
|
-
}
|
|
2183
|
-
// ============================================================================
|
|
2184
|
-
// Workflow Commands
|
|
2185
|
-
// ============================================================================
|
|
2186
|
-
async function runWorkflowDeploy(file, orgOverride) {
|
|
2187
|
-
if (!file) {
|
|
2188
|
-
console.error(chalk_1.default.red('Error: File path required'));
|
|
2189
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow deploy <file.yaml> [--org <id>]`));
|
|
2190
|
-
process.exit(2);
|
|
2191
|
-
}
|
|
2192
|
-
if (!fs.existsSync(file)) {
|
|
2193
|
-
console.error(chalk_1.default.red(`Error: File not found: ${file}`));
|
|
2194
|
-
process.exit(2);
|
|
2195
|
-
}
|
|
2196
|
-
const session = resolveSession();
|
|
2197
|
-
const domain = session.domain;
|
|
2198
|
-
const token = session.access_token;
|
|
2199
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2200
|
-
const yamlContent = fs.readFileSync(file, 'utf-8');
|
|
2201
|
-
const parsed = yaml_1.default.parse(yamlContent);
|
|
2202
|
-
const workflowId = parsed?.workflow?.workflowId;
|
|
2203
|
-
if (!workflowId) {
|
|
2204
|
-
console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
|
|
2205
|
-
process.exit(2);
|
|
2206
|
-
}
|
|
2207
|
-
const workflowName = parsed?.workflow?.name || workflowId;
|
|
2208
|
-
// Read app.yaml for appManifestId
|
|
2209
|
-
let appManifestId;
|
|
2210
|
-
const appYamlPath = path.join(process.cwd(), 'app.yaml');
|
|
2211
|
-
if (fs.existsSync(appYamlPath)) {
|
|
2212
|
-
const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
|
|
2213
|
-
appManifestId = appYaml?.id;
|
|
2214
|
-
}
|
|
2215
|
-
console.log(chalk_1.default.bold.cyan('\n Workflow Deploy\n'));
|
|
2216
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2217
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2218
|
-
console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
|
|
2219
|
-
console.log('');
|
|
2220
|
-
// Check if workflow exists
|
|
2221
|
-
const checkData = await graphqlRequest(domain, token, `
|
|
2222
|
-
query ($organizationId: Int!, $workflowId: UUID!) {
|
|
2223
|
-
workflow(organizationId: $organizationId, workflowId: $workflowId) {
|
|
2224
|
-
workflowId
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
`, { organizationId: orgId, workflowId });
|
|
2228
|
-
if (checkData?.workflow) {
|
|
2229
|
-
console.log(chalk_1.default.gray(' Updating existing workflow...'));
|
|
2230
|
-
const updateInput = {
|
|
2231
|
-
organizationId: orgId,
|
|
2232
|
-
workflowId,
|
|
2233
|
-
workflowYamlDocument: yamlContent,
|
|
2234
|
-
};
|
|
2235
|
-
if (appManifestId)
|
|
2236
|
-
updateInput.appManifestId = appManifestId;
|
|
2237
|
-
const result = await graphqlRequest(domain, token, `
|
|
2238
|
-
mutation ($input: UpdateWorkflowInput!) {
|
|
2239
|
-
updateWorkflow(input: $input) {
|
|
2240
|
-
workflow { workflowId }
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
`, {
|
|
2244
|
-
input: updateInput,
|
|
2245
|
-
});
|
|
2246
|
-
console.log(chalk_1.default.green(` ✓ Updated: ${workflowName}\n`));
|
|
2247
|
-
}
|
|
2248
|
-
else {
|
|
2249
|
-
console.log(chalk_1.default.gray(' Creating new workflow...'));
|
|
2250
|
-
const createInput = {
|
|
2251
|
-
organizationId: orgId,
|
|
2252
|
-
workflowYamlDocument: yamlContent,
|
|
2253
|
-
};
|
|
2254
|
-
if (appManifestId)
|
|
2255
|
-
createInput.appManifestId = appManifestId;
|
|
2256
|
-
const result = await graphqlRequest(domain, token, `
|
|
2257
|
-
mutation ($input: CreateWorkflowInput!) {
|
|
2258
|
-
createWorkflow(input: $input) {
|
|
2259
|
-
workflow { workflowId }
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
`, {
|
|
2263
|
-
input: createInput,
|
|
2264
|
-
});
|
|
2265
|
-
console.log(chalk_1.default.green(` ✓ Created: ${workflowName}\n`));
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
async function runWorkflowUndeploy(uuid, orgOverride) {
|
|
2269
|
-
if (!uuid) {
|
|
2270
|
-
console.error(chalk_1.default.red('Error: Workflow ID required'));
|
|
2271
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow undeploy <workflowId> [--org <id>]`));
|
|
2272
|
-
process.exit(2);
|
|
2273
|
-
}
|
|
2274
|
-
const session = resolveSession();
|
|
2275
|
-
const domain = session.domain;
|
|
2276
|
-
const token = session.access_token;
|
|
2277
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2278
|
-
console.log(chalk_1.default.bold.cyan('\n Workflow Undeploy\n'));
|
|
2279
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2280
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2281
|
-
console.log(chalk_1.default.gray(` Workflow: ${uuid}`));
|
|
2282
|
-
console.log('');
|
|
2283
|
-
await graphqlRequest(domain, token, `
|
|
2284
|
-
mutation ($input: DeleteWorkflowInput!) {
|
|
2285
|
-
deleteWorkflow(input: $input) {
|
|
2286
|
-
deleteResult { __typename }
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
`, {
|
|
2290
|
-
input: {
|
|
2291
|
-
organizationId: orgId,
|
|
2292
|
-
workflowId: uuid,
|
|
2293
|
-
},
|
|
2294
|
-
});
|
|
2295
|
-
console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
|
|
2296
|
-
}
|
|
2297
|
-
async function uploadFileToServer(domain, token, orgId, localPath) {
|
|
2298
|
-
const fileName = path.basename(localPath);
|
|
2299
|
-
const ext = path.extname(localPath).toLowerCase();
|
|
2300
|
-
const contentTypeMap = {
|
|
2301
|
-
'.csv': 'text/csv', '.json': 'application/json', '.xml': 'application/xml',
|
|
2302
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
2303
|
-
'.xls': 'application/vnd.ms-excel', '.pdf': 'application/pdf',
|
|
2304
|
-
'.txt': 'text/plain', '.zip': 'application/zip',
|
|
2305
|
-
};
|
|
2306
|
-
const contentType = contentTypeMap[ext] || 'application/octet-stream';
|
|
2307
|
-
// Step 1: Get presigned upload URL
|
|
2308
|
-
const data = await graphqlRequest(domain, token, `
|
|
2309
|
-
query ($organizationId: Int!, $fileName: String!, $contentType: String!) {
|
|
2310
|
-
uploadUrl(organizationId: $organizationId, fileName: $fileName, contentType: $contentType) {
|
|
2311
|
-
presignedUrl
|
|
2312
|
-
fileUrl
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
`, { organizationId: orgId, fileName, contentType });
|
|
2316
|
-
const presignedUrl = data?.uploadUrl?.presignedUrl;
|
|
2317
|
-
const fileUrl = data?.uploadUrl?.fileUrl;
|
|
2318
|
-
if (!presignedUrl || !fileUrl) {
|
|
2319
|
-
throw new Error('Failed to get upload URL from server');
|
|
2320
|
-
}
|
|
2321
|
-
// Step 2: PUT file content to presigned URL
|
|
2322
|
-
const fileContent = fs.readFileSync(localPath);
|
|
2323
|
-
const url = new URL(presignedUrl);
|
|
2324
|
-
const httpModule = url.protocol === 'https:' ? https : http;
|
|
2325
|
-
await new Promise((resolve, reject) => {
|
|
2326
|
-
const req = httpModule.request(url, {
|
|
2327
|
-
method: 'PUT',
|
|
2328
|
-
headers: {
|
|
2329
|
-
'Content-Type': contentType,
|
|
2330
|
-
'Content-Length': fileContent.length,
|
|
2331
|
-
'x-ms-blob-type': 'BlockBlob',
|
|
2332
|
-
},
|
|
2333
|
-
}, (res) => {
|
|
2334
|
-
let body = '';
|
|
2335
|
-
res.on('data', (chunk) => body += chunk);
|
|
2336
|
-
res.on('end', () => {
|
|
2337
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
2338
|
-
resolve();
|
|
2339
|
-
}
|
|
2340
|
-
else {
|
|
2341
|
-
reject(new Error(`File upload failed (${res.statusCode}): ${body}`));
|
|
2342
|
-
}
|
|
2343
|
-
});
|
|
2344
|
-
});
|
|
2345
|
-
req.on('error', reject);
|
|
2346
|
-
req.write(fileContent);
|
|
2347
|
-
req.end();
|
|
2348
|
-
});
|
|
2349
|
-
return fileUrl;
|
|
2350
|
-
}
|
|
2351
|
-
async function runWorkflowExecute(workflowIdOrFile, orgOverride, variables, fileArgs) {
|
|
2352
|
-
if (!workflowIdOrFile) {
|
|
2353
|
-
console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
|
|
2354
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow execute <workflowId|file.yaml> [--org <id>] [--vars '{"key":"value"}'] [--file varName=path]`));
|
|
2355
|
-
process.exit(2);
|
|
2356
|
-
}
|
|
2357
|
-
const session = resolveSession();
|
|
2358
|
-
const { domain, access_token: token } = session;
|
|
2359
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2360
|
-
// Resolve workflowId
|
|
2361
|
-
let workflowId = workflowIdOrFile;
|
|
2362
|
-
let workflowName = workflowIdOrFile;
|
|
2363
|
-
if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
|
|
2364
|
-
if (!fs.existsSync(workflowIdOrFile)) {
|
|
2365
|
-
console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
|
|
2366
|
-
process.exit(2);
|
|
2367
|
-
}
|
|
2368
|
-
const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
|
|
2369
|
-
workflowId = parsed?.workflow?.workflowId;
|
|
2370
|
-
workflowName = parsed?.workflow?.name || path.basename(workflowIdOrFile);
|
|
2371
|
-
if (!workflowId) {
|
|
2372
|
-
console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
|
|
2373
|
-
process.exit(2);
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
// Parse variables if provided
|
|
2377
|
-
let vars;
|
|
2378
|
-
if (variables) {
|
|
2379
|
-
try {
|
|
2380
|
-
vars = JSON.parse(variables);
|
|
2381
|
-
}
|
|
2382
|
-
catch {
|
|
2383
|
-
console.error(chalk_1.default.red('Error: --vars must be valid JSON'));
|
|
2384
|
-
process.exit(2);
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
// Process --file args: upload files and set URLs as variables
|
|
2388
|
-
if (fileArgs && fileArgs.length > 0) {
|
|
2389
|
-
if (!vars)
|
|
2390
|
-
vars = {};
|
|
2391
|
-
for (const fileArg of fileArgs) {
|
|
2392
|
-
const eqIdx = fileArg.indexOf('=');
|
|
2393
|
-
if (eqIdx < 1) {
|
|
2394
|
-
console.error(chalk_1.default.red(`Error: --file must be in format varName=path (got: ${fileArg})`));
|
|
2395
|
-
process.exit(2);
|
|
2396
|
-
}
|
|
2397
|
-
const varName = fileArg.substring(0, eqIdx);
|
|
2398
|
-
const filePath = fileArg.substring(eqIdx + 1);
|
|
2399
|
-
if (!fs.existsSync(filePath)) {
|
|
2400
|
-
console.error(chalk_1.default.red(`Error: File not found: ${filePath}`));
|
|
2401
|
-
process.exit(2);
|
|
2402
|
-
}
|
|
2403
|
-
console.log(chalk_1.default.gray(` Uploading ${path.basename(filePath)}...`));
|
|
2404
|
-
const fileUrl = await uploadFileToServer(domain, token, orgId, filePath);
|
|
2405
|
-
vars[varName] = fileUrl;
|
|
2406
|
-
console.log(chalk_1.default.gray(` → ${varName} = ${fileUrl}`));
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
console.log(chalk_1.default.bold.cyan('\n Workflow Execute\n'));
|
|
2410
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2411
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2412
|
-
console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
|
|
2413
|
-
if (vars)
|
|
2414
|
-
console.log(chalk_1.default.gray(` Variables: ${JSON.stringify(vars)}`));
|
|
2415
|
-
console.log('');
|
|
2416
|
-
const input = { organizationId: orgId, workflowId };
|
|
2417
|
-
if (vars)
|
|
2418
|
-
input.variables = vars;
|
|
2419
|
-
const data = await graphqlRequest(domain, token, `
|
|
2420
|
-
mutation ($input: ExecuteWorkflowInput!) {
|
|
2421
|
-
executeWorkflow(input: $input) {
|
|
2422
|
-
workflowExecutionResult {
|
|
2423
|
-
executionId workflowId isAsync outputs
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
`, { input });
|
|
2428
|
-
const result = data?.executeWorkflow?.workflowExecutionResult;
|
|
2429
|
-
if (!result) {
|
|
2430
|
-
console.error(chalk_1.default.red(' No execution result returned.\n'));
|
|
2431
|
-
process.exit(2);
|
|
2432
|
-
}
|
|
2433
|
-
console.log(chalk_1.default.green(` ✓ Executed: ${workflowName}`));
|
|
2434
|
-
console.log(chalk_1.default.white(` Execution ID: ${result.executionId}`));
|
|
2435
|
-
console.log(chalk_1.default.white(` Async: ${result.isAsync}`));
|
|
2436
|
-
if (result.outputs && Object.keys(result.outputs).length > 0) {
|
|
2437
|
-
console.log(chalk_1.default.white(` Outputs:`));
|
|
2438
|
-
console.log(chalk_1.default.gray(` ${JSON.stringify(result.outputs, null, 2).split('\n').join('\n ')}`));
|
|
2439
|
-
}
|
|
2440
|
-
console.log('');
|
|
2441
|
-
}
|
|
2442
|
-
function resolveWorkflowId(workflowIdOrFile) {
|
|
2443
|
-
if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
|
|
2444
|
-
if (!fs.existsSync(workflowIdOrFile)) {
|
|
2445
|
-
console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
|
|
2446
|
-
process.exit(2);
|
|
2447
|
-
}
|
|
2448
|
-
const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
|
|
2449
|
-
const id = parsed?.workflow?.workflowId;
|
|
2450
|
-
if (!id) {
|
|
2451
|
-
console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
|
|
2452
|
-
process.exit(2);
|
|
2453
|
-
}
|
|
2454
|
-
return id;
|
|
2455
|
-
}
|
|
2456
|
-
return workflowIdOrFile;
|
|
2457
|
-
}
|
|
2458
|
-
async function runWorkflowLogs(workflowIdOrFile, orgOverride, fromDate, toDate) {
|
|
2459
|
-
if (!workflowIdOrFile) {
|
|
2460
|
-
console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
|
|
2461
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow logs <workflowId|file.yaml> [--from <date>] [--to <date>]`));
|
|
2462
|
-
process.exit(2);
|
|
2463
|
-
}
|
|
2464
|
-
const session = resolveSession();
|
|
2465
|
-
const { domain, access_token: token } = session;
|
|
2466
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2467
|
-
const workflowId = resolveWorkflowId(workflowIdOrFile);
|
|
2468
|
-
// Parse date filters
|
|
2469
|
-
const fromTs = fromDate ? new Date(fromDate).getTime() : 0;
|
|
2470
|
-
const toTs = toDate ? new Date(toDate + 'T23:59:59').getTime() : Infinity;
|
|
2471
|
-
if (fromDate && isNaN(fromTs)) {
|
|
2472
|
-
console.error(chalk_1.default.red(`Invalid --from date: ${fromDate}. Use YYYY-MM-DD format.`));
|
|
2473
|
-
process.exit(2);
|
|
2474
|
-
}
|
|
2475
|
-
if (toDate && isNaN(toTs)) {
|
|
2476
|
-
console.error(chalk_1.default.red(`Invalid --to date: ${toDate}. Use YYYY-MM-DD format.`));
|
|
2477
|
-
process.exit(2);
|
|
2478
|
-
}
|
|
2479
|
-
const data = await graphqlRequest(domain, token, `
|
|
2480
|
-
query ($organizationId: Int!, $workflowId: UUID!) {
|
|
2481
|
-
workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 100) {
|
|
2482
|
-
totalCount
|
|
2483
|
-
items { executionId executionStatus executedAt durationMs txtLogUrl user { fullName email } }
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
`, { organizationId: orgId, workflowId });
|
|
2487
|
-
let items = data?.workflowExecutions?.items || [];
|
|
2488
|
-
const total = data?.workflowExecutions?.totalCount || 0;
|
|
2489
|
-
// Filter by date range
|
|
2490
|
-
if (fromDate || toDate) {
|
|
2491
|
-
items = items.filter((ex) => {
|
|
2492
|
-
const t = new Date(ex.executedAt).getTime();
|
|
2493
|
-
return t >= fromTs && t <= toTs;
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
// Sort descending
|
|
2497
|
-
items.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime());
|
|
2498
|
-
console.log(chalk_1.default.bold.cyan('\n Workflow Logs\n'));
|
|
2499
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2500
|
-
console.log(chalk_1.default.gray(` Workflow: ${workflowId}`));
|
|
2501
|
-
console.log(chalk_1.default.gray(` Total: ${total}`));
|
|
2502
|
-
if (fromDate || toDate) {
|
|
2503
|
-
console.log(chalk_1.default.gray(` Filter: ${fromDate || '...'} → ${toDate || '...'}`));
|
|
2504
|
-
}
|
|
2505
|
-
console.log(chalk_1.default.gray(` Showing: ${items.length}\n`));
|
|
2506
|
-
if (items.length === 0) {
|
|
2507
|
-
console.log(chalk_1.default.gray(' No executions found.\n'));
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
for (const ex of items) {
|
|
2511
|
-
const date = new Date(ex.executedAt).toLocaleString();
|
|
2512
|
-
const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
|
|
2513
|
-
const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
2514
|
-
const logIcon = ex.txtLogUrl ? chalk_1.default.green('●') : chalk_1.default.gray('○');
|
|
2515
|
-
const user = ex.user?.fullName || ex.user?.email || '';
|
|
2516
|
-
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) : ''}`);
|
|
2517
|
-
}
|
|
2518
|
-
console.log();
|
|
2519
|
-
console.log(chalk_1.default.gray(` ${chalk_1.default.green('●')} log available ${chalk_1.default.gray('○')} no log`));
|
|
2520
|
-
console.log(chalk_1.default.gray(` Download: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console]\n`));
|
|
2521
|
-
}
|
|
2522
|
-
function fetchGzipText(url) {
|
|
2523
|
-
const zlib = require('zlib');
|
|
2524
|
-
return new Promise((resolve, reject) => {
|
|
2525
|
-
const lib = url.startsWith('https') ? https : http;
|
|
2526
|
-
lib.get(url, (res) => {
|
|
2527
|
-
if (res.statusCode !== 200) {
|
|
2528
|
-
reject(new Error(`HTTP ${res.statusCode}`));
|
|
2529
|
-
res.resume();
|
|
2530
|
-
return;
|
|
2531
|
-
}
|
|
2532
|
-
const rawChunks = [];
|
|
2533
|
-
res.on('data', (chunk) => rawChunks.push(chunk));
|
|
2534
|
-
res.on('end', () => {
|
|
2535
|
-
const raw = Buffer.concat(rawChunks);
|
|
2536
|
-
if (raw.length === 0) {
|
|
2537
|
-
resolve('(empty log)');
|
|
2538
|
-
return;
|
|
2539
|
-
}
|
|
2540
|
-
zlib.gunzip(raw, (err, result) => {
|
|
2541
|
-
if (err) {
|
|
2542
|
-
resolve(raw.toString('utf-8'));
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
|
-
resolve(result.toString('utf-8'));
|
|
2546
|
-
});
|
|
2547
|
-
});
|
|
2548
|
-
}).on('error', reject);
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
|
-
async function runWorkflowLog(executionId, orgOverride, outputFile, toConsole, useJson) {
|
|
2552
|
-
if (!executionId) {
|
|
2553
|
-
console.error(chalk_1.default.red('Error: Execution ID required'));
|
|
2554
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console] [--json]`));
|
|
2555
|
-
process.exit(2);
|
|
2556
|
-
}
|
|
2557
|
-
const session = resolveSession();
|
|
2558
|
-
const { domain, access_token: token } = session;
|
|
2559
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2560
|
-
const data = await graphqlRequest(domain, token, `
|
|
2561
|
-
query ($organizationId: Int!, $executionId: UUID!) {
|
|
2562
|
-
workflowExecution(organizationId: $organizationId, executionId: $executionId) {
|
|
2563
|
-
executionId workflowId executionStatus executedAt durationMs
|
|
2564
|
-
txtLogUrl jsonLogUrl
|
|
2565
|
-
user { fullName email }
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
`, { organizationId: orgId, executionId });
|
|
2569
|
-
const ex = data?.workflowExecution;
|
|
2570
|
-
if (!ex) {
|
|
2571
|
-
console.error(chalk_1.default.red(`Execution not found: ${executionId}`));
|
|
2572
|
-
process.exit(2);
|
|
2573
|
-
}
|
|
2574
|
-
const logUrl = useJson ? ex.jsonLogUrl : ex.txtLogUrl;
|
|
2575
|
-
const logType = useJson ? 'json' : 'txt';
|
|
2576
|
-
const ext = useJson ? '.json' : '.log';
|
|
2577
|
-
if (!logUrl) {
|
|
2578
|
-
console.error(chalk_1.default.yellow(`No ${logType} log available for this execution.`));
|
|
2579
|
-
process.exit(0);
|
|
2580
|
-
}
|
|
2581
|
-
const date = new Date(ex.executedAt).toLocaleString();
|
|
2582
|
-
const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
|
|
2583
|
-
const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
2584
|
-
const userName = ex.user?.fullName || ex.user?.email || '';
|
|
2585
|
-
// Download log
|
|
2586
|
-
let logText;
|
|
2587
|
-
try {
|
|
2588
|
-
logText = await fetchGzipText(logUrl);
|
|
2589
|
-
}
|
|
2590
|
-
catch (e) {
|
|
2591
|
-
console.error(chalk_1.default.red(`Failed to download log: ${e.message}`));
|
|
2592
|
-
process.exit(2);
|
|
2593
|
-
}
|
|
2594
|
-
// Pretty-print JSON if it's valid JSON
|
|
2595
|
-
if (useJson) {
|
|
2596
|
-
try {
|
|
2597
|
-
const parsed = JSON.parse(logText);
|
|
2598
|
-
logText = JSON.stringify(parsed, null, 2);
|
|
2599
|
-
}
|
|
2600
|
-
catch { /* keep as-is */ }
|
|
2601
|
-
}
|
|
2602
|
-
if (toConsole) {
|
|
2603
|
-
console.log(chalk_1.default.bold.cyan('\n Workflow Execution\n'));
|
|
2604
|
-
console.log(chalk_1.default.white(` ID: ${ex.executionId}`));
|
|
2605
|
-
console.log(chalk_1.default.white(` Workflow: ${ex.workflowId}`));
|
|
2606
|
-
console.log(chalk_1.default.white(` Status: ${statusColor(ex.executionStatus)}`));
|
|
2607
|
-
console.log(chalk_1.default.white(` Executed: ${date}`));
|
|
2608
|
-
console.log(chalk_1.default.white(` Duration: ${duration}`));
|
|
2609
|
-
if (userName)
|
|
2610
|
-
console.log(chalk_1.default.white(` User: ${userName}`));
|
|
2611
|
-
console.log(chalk_1.default.gray(`\n --- ${logType.toUpperCase()} Log ---\n`));
|
|
2612
|
-
console.log(logText);
|
|
2613
|
-
return;
|
|
2614
|
-
}
|
|
2615
|
-
// Save to file
|
|
2616
|
-
let filePath;
|
|
2617
|
-
if (outputFile) {
|
|
2618
|
-
filePath = path.resolve(outputFile);
|
|
2619
|
-
}
|
|
2620
|
-
else {
|
|
2621
|
-
const tmpDir = os.tmpdir();
|
|
2622
|
-
const dateStr = new Date(ex.executedAt).toISOString().slice(0, 10);
|
|
2623
|
-
filePath = path.join(tmpDir, `workflow-${ex.workflowId}-${dateStr}-${executionId}${ext}`);
|
|
2624
|
-
}
|
|
2625
|
-
fs.writeFileSync(filePath, logText, 'utf-8');
|
|
2626
|
-
console.log(chalk_1.default.green(` ✓ ${logType.toUpperCase()} log saved: ${filePath}`));
|
|
2627
|
-
console.log(chalk_1.default.gray(` Execution: ${executionId} ${statusColor(ex.executionStatus)} ${date} ${duration}`));
|
|
2628
|
-
}
|
|
2629
|
-
// ============================================================================
|
|
2630
|
-
// Publish Command
|
|
2631
|
-
// ============================================================================
|
|
2632
|
-
async function pushWorkflowQuiet(domain, token, orgId, file, appManifestId) {
|
|
2633
|
-
let name = path.basename(file);
|
|
2634
|
-
try {
|
|
2635
|
-
const yamlContent = fs.readFileSync(file, 'utf-8');
|
|
2636
|
-
const parsed = yaml_1.default.parse(yamlContent);
|
|
2637
|
-
const workflowId = parsed?.workflow?.workflowId;
|
|
2638
|
-
name = parsed?.workflow?.name || name;
|
|
2639
|
-
if (!workflowId)
|
|
2640
|
-
return { ok: false, name, error: 'Missing workflow.workflowId' };
|
|
2641
|
-
const checkData = await graphqlRequest(domain, token, `
|
|
2642
|
-
query ($organizationId: Int!, $workflowId: UUID!) {
|
|
2643
|
-
workflow(organizationId: $organizationId, workflowId: $workflowId) { workflowId }
|
|
2644
|
-
}
|
|
2645
|
-
`, { organizationId: orgId, workflowId });
|
|
2646
|
-
if (checkData?.workflow) {
|
|
2647
|
-
const updateInput = { organizationId: orgId, workflowId, workflowYamlDocument: yamlContent };
|
|
2648
|
-
if (appManifestId)
|
|
2649
|
-
updateInput.appManifestId = appManifestId;
|
|
2650
|
-
await graphqlRequest(domain, token, `
|
|
2651
|
-
mutation ($input: UpdateWorkflowInput!) {
|
|
2652
|
-
updateWorkflow(input: $input) { workflow { workflowId } }
|
|
2653
|
-
}
|
|
2654
|
-
`, { input: updateInput });
|
|
2655
|
-
}
|
|
2656
|
-
else {
|
|
2657
|
-
const createInput = { organizationId: orgId, workflowYamlDocument: yamlContent };
|
|
2658
|
-
if (appManifestId)
|
|
2659
|
-
createInput.appManifestId = appManifestId;
|
|
2660
|
-
await graphqlRequest(domain, token, `
|
|
2661
|
-
mutation ($input: CreateWorkflowInput!) {
|
|
2662
|
-
createWorkflow(input: $input) { workflow { workflowId } }
|
|
2663
|
-
}
|
|
2664
|
-
`, { input: createInput });
|
|
2665
|
-
}
|
|
2666
|
-
return { ok: true, name };
|
|
2667
|
-
}
|
|
2668
|
-
catch (e) {
|
|
2669
|
-
return { ok: false, name, error: e.message };
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
async function pushModuleQuiet(domain, token, orgId, file, appManifestId) {
|
|
2673
|
-
let name = path.basename(file);
|
|
2674
|
-
try {
|
|
2675
|
-
const yamlContent = fs.readFileSync(file, 'utf-8');
|
|
2676
|
-
const parsed = yaml_1.default.parse(yamlContent);
|
|
2677
|
-
const appModuleId = parsed?.module?.appModuleId;
|
|
2678
|
-
name = parsed?.module?.name || name;
|
|
2679
|
-
if (!appModuleId)
|
|
2680
|
-
return { ok: false, name, error: 'Missing module.appModuleId' };
|
|
2681
|
-
const checkData = await graphqlRequest(domain, token, `
|
|
2682
|
-
query ($organizationId: Int!, $appModuleId: UUID!) {
|
|
2683
|
-
appModule(organizationId: $organizationId, appModuleId: $appModuleId) { appModuleId }
|
|
2684
|
-
}
|
|
2685
|
-
`, { organizationId: orgId, appModuleId });
|
|
2686
|
-
if (checkData?.appModule) {
|
|
2687
|
-
const updateValues = { appModuleYamlDocument: yamlContent };
|
|
2688
|
-
if (appManifestId)
|
|
2689
|
-
updateValues.appManifestId = appManifestId;
|
|
2690
|
-
await graphqlRequest(domain, token, `
|
|
2691
|
-
mutation ($input: UpdateAppModuleInput!) {
|
|
2692
|
-
updateAppModule(input: $input) { appModule { appModuleId name } }
|
|
2693
|
-
}
|
|
2694
|
-
`, { input: { organizationId: orgId, appModuleId, values: updateValues } });
|
|
2695
|
-
}
|
|
2696
|
-
else {
|
|
2697
|
-
const values = { appModuleYamlDocument: yamlContent };
|
|
2698
|
-
if (appManifestId)
|
|
2699
|
-
values.appManifestId = appManifestId;
|
|
2700
|
-
await graphqlRequest(domain, token, `
|
|
2701
|
-
mutation ($input: CreateAppModuleInput!) {
|
|
2702
|
-
createAppModule(input: $input) { appModule { appModuleId name } }
|
|
2703
|
-
}
|
|
2704
|
-
`, { input: { organizationId: orgId, values } });
|
|
2705
|
-
}
|
|
2706
|
-
return { ok: true, name };
|
|
2707
|
-
}
|
|
2708
|
-
catch (e) {
|
|
2709
|
-
return { ok: false, name, error: e.message };
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
// ============================================================================
|
|
2713
|
-
// PAT Token Commands
|
|
2714
|
-
// ============================================================================
|
|
2715
|
-
async function runPatCreate(name) {
|
|
2716
|
-
const session = resolveSession();
|
|
2717
|
-
const { domain, access_token: token } = session;
|
|
2718
|
-
const data = await graphqlRequest(domain, token, `
|
|
2719
|
-
mutation ($input: CreatePersonalAccessTokenInput!) {
|
|
2720
|
-
createPersonalAccessToken(input: $input) {
|
|
2721
|
-
createPatPayload {
|
|
2722
|
-
token
|
|
2723
|
-
personalAccessToken { id name scopes }
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
`, { input: { input: { name, scopes: ['TMS.ApiAPI'] } } });
|
|
2728
|
-
const payload = data?.createPersonalAccessToken?.createPatPayload;
|
|
2729
|
-
const patToken = payload?.token;
|
|
2730
|
-
const pat = payload?.personalAccessToken;
|
|
2731
|
-
if (!patToken) {
|
|
2732
|
-
console.error(chalk_1.default.red('Failed to create PAT token — no token returned.'));
|
|
2733
|
-
process.exit(2);
|
|
2734
|
-
}
|
|
2735
|
-
console.log(chalk_1.default.green('PAT token created successfully!'));
|
|
2736
|
-
console.log();
|
|
2737
|
-
console.log(chalk_1.default.bold(' Token:'), chalk_1.default.cyan(patToken));
|
|
2738
|
-
console.log(chalk_1.default.bold(' ID: '), chalk_1.default.gray(pat?.id || 'unknown'));
|
|
2739
|
-
console.log(chalk_1.default.bold(' Name: '), pat?.name || name);
|
|
2740
|
-
console.log();
|
|
2741
|
-
console.log(chalk_1.default.yellow('⚠ Copy the token now — it will not be shown again.'));
|
|
2742
|
-
console.log();
|
|
2743
|
-
console.log(chalk_1.default.bold('To use PAT authentication, add to your project .env file:'));
|
|
2744
|
-
console.log();
|
|
2745
|
-
console.log(chalk_1.default.cyan(` CXTMS_AUTH=${patToken}`));
|
|
2746
|
-
console.log(chalk_1.default.cyan(` CXTMS_SERVER=${domain}`));
|
|
2747
|
-
console.log();
|
|
2748
|
-
console.log(chalk_1.default.gray('When CXTMS_AUTH is set, cxtms will skip OAuth login and use the PAT token directly.'));
|
|
2749
|
-
console.log(chalk_1.default.gray('You can also export these as environment variables instead of using .env.'));
|
|
2750
|
-
}
|
|
2751
|
-
async function runPatList() {
|
|
2752
|
-
const session = resolveSession();
|
|
2753
|
-
const { domain, access_token: token } = session;
|
|
2754
|
-
const data = await graphqlRequest(domain, token, `
|
|
2755
|
-
{
|
|
2756
|
-
personalAccessTokens(skip: 0, take: 50) {
|
|
2757
|
-
items { id name createdAt expiresAt lastUsedAt scopes }
|
|
2758
|
-
totalCount
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
`, {});
|
|
2762
|
-
const items = data?.personalAccessTokens?.items || [];
|
|
2763
|
-
const total = data?.personalAccessTokens?.totalCount ?? items.length;
|
|
2764
|
-
if (items.length === 0) {
|
|
2765
|
-
console.log(chalk_1.default.gray('No active PAT tokens found.'));
|
|
2766
|
-
return;
|
|
2767
|
-
}
|
|
2768
|
-
console.log(chalk_1.default.bold(`PAT tokens (${total}):\n`));
|
|
2769
|
-
for (const t of items) {
|
|
2770
|
-
const expires = t.expiresAt ? new Date(t.expiresAt).toLocaleDateString() : 'never';
|
|
2771
|
-
const lastUsed = t.lastUsedAt ? new Date(t.lastUsedAt).toLocaleDateString() : 'never';
|
|
2772
|
-
console.log(` ${chalk_1.default.cyan(t.name || '(unnamed)')}`);
|
|
2773
|
-
console.log(` ID: ${chalk_1.default.gray(t.id)}`);
|
|
2774
|
-
console.log(` Created: ${new Date(t.createdAt).toLocaleDateString()}`);
|
|
2775
|
-
console.log(` Expires: ${expires}`);
|
|
2776
|
-
console.log(` Last used: ${lastUsed}`);
|
|
2777
|
-
console.log(` Scopes: ${(t.scopes || []).join(', ') || 'none'}`);
|
|
2778
|
-
console.log();
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
async function runPatRevoke(id) {
|
|
2782
|
-
const session = resolveSession();
|
|
2783
|
-
const { domain, access_token: token } = session;
|
|
2784
|
-
const data = await graphqlRequest(domain, token, `
|
|
2785
|
-
mutation ($input: RevokePersonalAccessTokenInput!) {
|
|
2786
|
-
revokePersonalAccessToken(input: $input) {
|
|
2787
|
-
personalAccessToken { id name revokedAt }
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
`, { input: { id } });
|
|
2791
|
-
const revoked = data?.revokePersonalAccessToken?.personalAccessToken;
|
|
2792
|
-
if (revoked) {
|
|
2793
|
-
console.log(chalk_1.default.green(`PAT token revoked: ${revoked.name || revoked.id}`));
|
|
2794
|
-
}
|
|
2795
|
-
else {
|
|
2796
|
-
console.log(chalk_1.default.green('PAT token revoked.'));
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
async function runPatSetup() {
|
|
2800
|
-
const patToken = process.env.CXTMS_AUTH;
|
|
2801
|
-
const server = process.env.CXTMS_SERVER || resolveDomainFromAppYaml();
|
|
2802
|
-
console.log(chalk_1.default.bold('PAT Token Status:\n'));
|
|
2803
|
-
if (patToken) {
|
|
2804
|
-
const masked = patToken.slice(0, 8) + '...' + patToken.slice(-4);
|
|
2805
|
-
console.log(chalk_1.default.green(` CXTMS_AUTH is set: ${masked}`));
|
|
2806
|
-
}
|
|
2807
|
-
else {
|
|
2808
|
-
console.log(chalk_1.default.yellow(' CXTMS_AUTH is not set'));
|
|
2809
|
-
}
|
|
2810
|
-
if (server) {
|
|
2811
|
-
console.log(chalk_1.default.green(` Server: ${server}`));
|
|
2812
|
-
}
|
|
2813
|
-
else {
|
|
2814
|
-
console.log(chalk_1.default.yellow(' Server: not configured (add `server` to app.yaml or set CXTMS_SERVER)'));
|
|
2815
|
-
}
|
|
2816
|
-
console.log();
|
|
2817
|
-
if (patToken && server) {
|
|
2818
|
-
console.log(chalk_1.default.green('PAT authentication is active. OAuth login will be skipped.'));
|
|
2819
|
-
}
|
|
2820
|
-
else {
|
|
2821
|
-
console.log(chalk_1.default.bold('To set up PAT authentication:'));
|
|
2822
|
-
console.log();
|
|
2823
|
-
console.log(chalk_1.default.white(' 1. Create a token:'));
|
|
2824
|
-
console.log(chalk_1.default.cyan(' cxtms pat create "my-token-name"'));
|
|
2825
|
-
console.log();
|
|
2826
|
-
console.log(chalk_1.default.white(' 2. Add to your project .env file:'));
|
|
2827
|
-
console.log(chalk_1.default.cyan(' CXTMS_AUTH=pat_xxxxx'));
|
|
2828
|
-
console.log(chalk_1.default.cyan(' CXTMS_SERVER=https://your-server.com'));
|
|
2829
|
-
console.log();
|
|
2830
|
-
console.log(chalk_1.default.gray(' Or set `server` in app.yaml instead of CXTMS_SERVER.'));
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
async function runPublish(featureDir, orgOverride) {
|
|
2834
|
-
const session = resolveSession();
|
|
2835
|
-
const domain = session.domain;
|
|
2836
|
-
const token = session.access_token;
|
|
2837
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2838
|
-
// Read app.yaml
|
|
2839
|
-
const appYamlPath = path.join(process.cwd(), 'app.yaml');
|
|
2840
|
-
if (!fs.existsSync(appYamlPath)) {
|
|
2841
|
-
console.error(chalk_1.default.red('Error: app.yaml not found in current directory'));
|
|
2842
|
-
process.exit(2);
|
|
2843
|
-
}
|
|
2844
|
-
const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
|
|
2845
|
-
const appManifestId = appYaml?.id;
|
|
2846
|
-
const appName = appYaml?.name || 'unknown';
|
|
2847
|
-
console.log(chalk_1.default.bold.cyan('\n Publish\n'));
|
|
2848
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2849
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2850
|
-
console.log(chalk_1.default.gray(` App: ${appName}`));
|
|
2851
|
-
if (featureDir) {
|
|
2852
|
-
console.log(chalk_1.default.gray(` Feature: ${featureDir}`));
|
|
2853
|
-
}
|
|
2854
|
-
console.log('');
|
|
2855
|
-
// Step 1: Create or update app manifest
|
|
2856
|
-
if (appManifestId) {
|
|
2857
|
-
console.log(chalk_1.default.gray(' Publishing app manifest...'));
|
|
2858
|
-
try {
|
|
2859
|
-
const checkData = await graphqlRequest(domain, token, `
|
|
2860
|
-
query ($organizationId: Int!, $appManifestId: UUID!) {
|
|
2861
|
-
appManifest(organizationId: $organizationId, appManifestId: $appManifestId) { appManifestId }
|
|
2862
|
-
}
|
|
2863
|
-
`, { organizationId: orgId, appManifestId });
|
|
2864
|
-
if (checkData?.appManifest) {
|
|
2865
|
-
await graphqlRequest(domain, token, `
|
|
2866
|
-
mutation ($input: UpdateAppManifestInput!) {
|
|
2867
|
-
updateAppManifest(input: $input) { appManifest { appManifestId name } }
|
|
2868
|
-
}
|
|
2869
|
-
`, { input: { organizationId: orgId, appManifestId, values: { name: appName, description: appYaml?.description || '' } } });
|
|
2870
|
-
console.log(chalk_1.default.green(' ✓ App manifest updated'));
|
|
2871
|
-
}
|
|
2872
|
-
else {
|
|
2873
|
-
await graphqlRequest(domain, token, `
|
|
2874
|
-
mutation ($input: CreateAppManifestInput!) {
|
|
2875
|
-
createAppManifest(input: $input) { appManifest { appManifestId name } }
|
|
2876
|
-
}
|
|
2877
|
-
`, { input: { organizationId: orgId, values: { appManifestId, name: appName, description: appYaml?.description || '' } } });
|
|
2878
|
-
console.log(chalk_1.default.green(' ✓ App manifest created'));
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
catch (e) {
|
|
2882
|
-
console.log(chalk_1.default.red(` ✗ App manifest failed: ${e.message}`));
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
// Step 2: Discover files
|
|
2886
|
-
const baseDir = featureDir ? path.join(process.cwd(), 'features', featureDir) : process.cwd();
|
|
2887
|
-
if (featureDir && !fs.existsSync(baseDir)) {
|
|
2888
|
-
console.error(chalk_1.default.red(`Error: Feature directory not found: features/${featureDir}`));
|
|
2889
|
-
process.exit(2);
|
|
2890
|
-
}
|
|
2891
|
-
const workflowDirs = [path.join(baseDir, 'workflows')];
|
|
2892
|
-
const moduleDirs = [path.join(baseDir, 'modules')];
|
|
2893
|
-
// Collect YAML files
|
|
2894
|
-
const workflowFiles = [];
|
|
2895
|
-
const moduleFiles = [];
|
|
2896
|
-
for (const dir of workflowDirs) {
|
|
2897
|
-
if (fs.existsSync(dir)) {
|
|
2898
|
-
for (const f of fs.readdirSync(dir)) {
|
|
2899
|
-
if (f.endsWith('.yaml') || f.endsWith('.yml')) {
|
|
2900
|
-
workflowFiles.push(path.join(dir, f));
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
for (const dir of moduleDirs) {
|
|
2906
|
-
if (fs.existsSync(dir)) {
|
|
2907
|
-
for (const f of fs.readdirSync(dir)) {
|
|
2908
|
-
if (f.endsWith('.yaml') || f.endsWith('.yml')) {
|
|
2909
|
-
moduleFiles.push(path.join(dir, f));
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
console.log(chalk_1.default.gray(`\n Found ${workflowFiles.length} workflow(s), ${moduleFiles.length} module(s)\n`));
|
|
2915
|
-
let succeeded = 0;
|
|
2916
|
-
let failed = 0;
|
|
2917
|
-
// Step 3: Deploy workflows
|
|
2918
|
-
for (const file of workflowFiles) {
|
|
2919
|
-
const relPath = path.relative(process.cwd(), file);
|
|
2920
|
-
const result = await pushWorkflowQuiet(domain, token, orgId, file, appManifestId);
|
|
2921
|
-
if (result.ok) {
|
|
2922
|
-
console.log(chalk_1.default.green(` ✓ ${relPath}`));
|
|
2923
|
-
succeeded++;
|
|
2924
|
-
}
|
|
2925
|
-
else {
|
|
2926
|
-
console.log(chalk_1.default.red(` ✗ ${relPath}: ${result.error}`));
|
|
2927
|
-
failed++;
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
// Step 4: Deploy modules
|
|
2931
|
-
for (const file of moduleFiles) {
|
|
2932
|
-
const relPath = path.relative(process.cwd(), file);
|
|
2933
|
-
const result = await pushModuleQuiet(domain, token, orgId, file, appManifestId);
|
|
2934
|
-
if (result.ok) {
|
|
2935
|
-
console.log(chalk_1.default.green(` ✓ ${relPath}`));
|
|
2936
|
-
succeeded++;
|
|
2937
|
-
}
|
|
2938
|
-
else {
|
|
2939
|
-
console.log(chalk_1.default.red(` ✗ ${relPath}: ${result.error}`));
|
|
2940
|
-
failed++;
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
// Summary
|
|
2944
|
-
console.log('');
|
|
2945
|
-
if (failed === 0) {
|
|
2946
|
-
console.log(chalk_1.default.green(` ✓ Published ${succeeded} file(s) successfully\n`));
|
|
2947
|
-
}
|
|
2948
|
-
else {
|
|
2949
|
-
console.log(chalk_1.default.yellow(` Published ${succeeded} file(s), ${failed} failed\n`));
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
// ============================================================================
|
|
2953
|
-
// App Manifest Commands (install from git, publish to git, list)
|
|
2954
|
-
// ============================================================================
|
|
2955
|
-
function readAppYaml() {
|
|
2956
|
-
const appYamlPath = path.join(process.cwd(), 'app.yaml');
|
|
2957
|
-
if (!fs.existsSync(appYamlPath)) {
|
|
2958
|
-
console.error(chalk_1.default.red('Error: app.yaml not found in current directory'));
|
|
2959
|
-
process.exit(2);
|
|
2960
|
-
}
|
|
2961
|
-
return yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
|
|
2962
|
-
}
|
|
2963
|
-
async function runAppInstall(orgOverride, branch, force, skipChanged) {
|
|
2964
|
-
const session = resolveSession();
|
|
2965
|
-
const domain = session.domain;
|
|
2966
|
-
const token = session.access_token;
|
|
2967
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2968
|
-
const appYaml = readAppYaml();
|
|
2969
|
-
const repository = appYaml.repository;
|
|
2970
|
-
if (!repository) {
|
|
2971
|
-
console.error(chalk_1.default.red('Error: app.yaml must have a `repository` field'));
|
|
2972
|
-
process.exit(2);
|
|
2973
|
-
}
|
|
2974
|
-
const repositoryBranch = branch || appYaml.branch || 'main';
|
|
2975
|
-
console.log(chalk_1.default.bold.cyan('\n App Install\n'));
|
|
2976
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2977
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2978
|
-
console.log(chalk_1.default.gray(` Repository: ${repository}`));
|
|
2979
|
-
console.log(chalk_1.default.gray(` Branch: ${repositoryBranch}`));
|
|
2980
|
-
if (force)
|
|
2981
|
-
console.log(chalk_1.default.gray(` Force: yes`));
|
|
2982
|
-
if (skipChanged)
|
|
2983
|
-
console.log(chalk_1.default.gray(` Skip changed: yes`));
|
|
2984
|
-
console.log('');
|
|
2985
|
-
try {
|
|
2986
|
-
const data = await graphqlRequest(domain, token, `
|
|
2987
|
-
mutation ($input: InstallAppManifestInput!) {
|
|
2988
|
-
installAppManifest(input: $input) {
|
|
2989
|
-
appManifest {
|
|
2990
|
-
appManifestId
|
|
2991
|
-
name
|
|
2992
|
-
currentVersion
|
|
2993
|
-
isEnabled
|
|
2994
|
-
hasUnpublishedChanges
|
|
2995
|
-
isUpdateAvailable
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
`, {
|
|
3000
|
-
input: {
|
|
3001
|
-
organizationId: orgId,
|
|
3002
|
-
values: {
|
|
3003
|
-
repository,
|
|
3004
|
-
repositoryBranch,
|
|
3005
|
-
force: force || false,
|
|
3006
|
-
skipModulesWithChanges: skipChanged || false,
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
});
|
|
3010
|
-
const manifest = data?.installAppManifest?.appManifest;
|
|
3011
|
-
if (manifest) {
|
|
3012
|
-
console.log(chalk_1.default.green(` ✓ Installed ${manifest.name} v${manifest.currentVersion}`));
|
|
3013
|
-
if (manifest.hasUnpublishedChanges) {
|
|
3014
|
-
console.log(chalk_1.default.yellow(` Has unpublished changes`));
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
else {
|
|
3018
|
-
console.log(chalk_1.default.green(' ✓ Install completed'));
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
catch (e) {
|
|
3022
|
-
console.error(chalk_1.default.red(` ✗ Install failed: ${e.message}`));
|
|
3023
|
-
process.exit(1);
|
|
3024
|
-
}
|
|
3025
|
-
console.log('');
|
|
3026
|
-
}
|
|
3027
|
-
async function runAppPublish(orgOverride, message, branch, force, targetFiles) {
|
|
3028
|
-
const session = resolveSession();
|
|
3029
|
-
const domain = session.domain;
|
|
3030
|
-
const token = session.access_token;
|
|
3031
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
3032
|
-
if (!message) {
|
|
3033
|
-
console.error(chalk_1.default.red('Error: --message (-m) is required for app release'));
|
|
3034
|
-
console.error(chalk_1.default.gray('Describe what changed, similar to a git commit message.'));
|
|
3035
|
-
console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} app release -m "Add new shipping module"`));
|
|
3036
|
-
process.exit(2);
|
|
3037
|
-
}
|
|
3038
|
-
const appYaml = readAppYaml();
|
|
3039
|
-
const appManifestId = appYaml.id;
|
|
3040
|
-
if (!appManifestId) {
|
|
3041
|
-
console.error(chalk_1.default.red('Error: app.yaml must have an `id` field'));
|
|
3042
|
-
process.exit(2);
|
|
3043
|
-
}
|
|
3044
|
-
console.log(chalk_1.default.bold.cyan('\n App Release\n'));
|
|
3045
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
3046
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
3047
|
-
console.log(chalk_1.default.gray(` App: ${appYaml.name || appManifestId}`));
|
|
3048
|
-
if (message)
|
|
3049
|
-
console.log(chalk_1.default.gray(` Message: ${message}`));
|
|
3050
|
-
if (branch)
|
|
3051
|
-
console.log(chalk_1.default.gray(` Branch: ${branch}`));
|
|
3052
|
-
if (force)
|
|
3053
|
-
console.log(chalk_1.default.gray(` Force: yes`));
|
|
3054
|
-
// Extract workflow/module IDs from target files
|
|
3055
|
-
const workflowIds = [];
|
|
3056
|
-
const moduleIds = [];
|
|
3057
|
-
if (targetFiles && targetFiles.length > 0) {
|
|
3058
|
-
for (const file of targetFiles) {
|
|
3059
|
-
if (!fs.existsSync(file)) {
|
|
3060
|
-
console.error(chalk_1.default.red(` Error: File not found: ${file}`));
|
|
3061
|
-
process.exit(2);
|
|
3062
|
-
}
|
|
3063
|
-
const parsed = yaml_1.default.parse(fs.readFileSync(file, 'utf-8'));
|
|
3064
|
-
if (parsed?.workflow?.workflowId) {
|
|
3065
|
-
workflowIds.push(parsed.workflow.workflowId);
|
|
3066
|
-
console.log(chalk_1.default.gray(` Workflow: ${parsed.workflow.name || parsed.workflow.workflowId}`));
|
|
3067
|
-
}
|
|
3068
|
-
else if (parsed?.module?.appModuleId) {
|
|
3069
|
-
moduleIds.push(parsed.module.appModuleId);
|
|
3070
|
-
console.log(chalk_1.default.gray(` Module: ${parsed.module.name || parsed.module.appModuleId}`));
|
|
3071
|
-
}
|
|
3072
|
-
else {
|
|
3073
|
-
console.error(chalk_1.default.red(` Error: Cannot identify file type: ${file}`));
|
|
3074
|
-
process.exit(2);
|
|
3075
|
-
}
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
console.log('');
|
|
3079
|
-
try {
|
|
3080
|
-
const publishValues = {
|
|
3081
|
-
message: message || undefined,
|
|
3082
|
-
branch: branch || undefined,
|
|
3083
|
-
force: force || false,
|
|
3084
|
-
};
|
|
3085
|
-
if (workflowIds.length > 0)
|
|
3086
|
-
publishValues.workflowIds = workflowIds;
|
|
3087
|
-
if (moduleIds.length > 0)
|
|
3088
|
-
publishValues.moduleIds = moduleIds;
|
|
3089
|
-
const data = await graphqlRequest(domain, token, `
|
|
3090
|
-
mutation ($input: PublishAppManifestInput!) {
|
|
3091
|
-
publishAppManifest(input: $input) {
|
|
3092
|
-
appManifest {
|
|
3093
|
-
appManifestId
|
|
3094
|
-
name
|
|
3095
|
-
currentVersion
|
|
3096
|
-
hasUnpublishedChanges
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
`, {
|
|
3101
|
-
input: {
|
|
3102
|
-
organizationId: orgId,
|
|
3103
|
-
appManifestId,
|
|
3104
|
-
values: publishValues,
|
|
3105
|
-
}
|
|
3106
|
-
});
|
|
3107
|
-
const manifest = data?.publishAppManifest?.appManifest;
|
|
3108
|
-
if (manifest) {
|
|
3109
|
-
console.log(chalk_1.default.green(` ✓ Published ${manifest.name} v${manifest.currentVersion}`));
|
|
3110
|
-
}
|
|
3111
|
-
else {
|
|
3112
|
-
console.log(chalk_1.default.green(' ✓ Publish completed'));
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
catch (e) {
|
|
3116
|
-
console.error(chalk_1.default.red(` ✗ Publish failed: ${e.message}`));
|
|
3117
|
-
process.exit(1);
|
|
3118
|
-
}
|
|
3119
|
-
console.log('');
|
|
3120
|
-
}
|
|
3121
|
-
async function runAppList(orgOverride) {
|
|
3122
|
-
const session = resolveSession();
|
|
3123
|
-
const domain = session.domain;
|
|
3124
|
-
const token = session.access_token;
|
|
3125
|
-
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
3126
|
-
console.log(chalk_1.default.bold.cyan('\n App Manifests\n'));
|
|
3127
|
-
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
3128
|
-
console.log(chalk_1.default.gray(` Org: ${orgId}\n`));
|
|
3129
|
-
try {
|
|
3130
|
-
const data = await graphqlRequest(domain, token, `
|
|
3131
|
-
query ($organizationId: Int!) {
|
|
3132
|
-
appManifests(organizationId: $organizationId) {
|
|
3133
|
-
items {
|
|
3134
|
-
appManifestId
|
|
3135
|
-
name
|
|
3136
|
-
currentVersion
|
|
3137
|
-
isEnabled
|
|
3138
|
-
hasUnpublishedChanges
|
|
3139
|
-
isUpdateAvailable
|
|
3140
|
-
repository
|
|
3141
|
-
repositoryBranch
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
`, { organizationId: orgId });
|
|
3146
|
-
const items = data?.appManifests?.items || [];
|
|
3147
|
-
if (items.length === 0) {
|
|
3148
|
-
console.log(chalk_1.default.gray(' No app manifests installed\n'));
|
|
3149
|
-
return;
|
|
3150
|
-
}
|
|
3151
|
-
for (const app of items) {
|
|
3152
|
-
const flags = [];
|
|
3153
|
-
if (!app.isEnabled)
|
|
3154
|
-
flags.push(chalk_1.default.red('disabled'));
|
|
3155
|
-
if (app.hasUnpublishedChanges)
|
|
3156
|
-
flags.push(chalk_1.default.yellow('unpublished'));
|
|
3157
|
-
if (app.isUpdateAvailable)
|
|
3158
|
-
flags.push(chalk_1.default.cyan('update available'));
|
|
3159
|
-
const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
|
|
3160
|
-
console.log(` ${chalk_1.default.bold(app.name)} ${chalk_1.default.gray(`v${app.currentVersion}`)}${flagStr}`);
|
|
3161
|
-
console.log(chalk_1.default.gray(` ID: ${app.appManifestId}`));
|
|
3162
|
-
if (app.repository) {
|
|
3163
|
-
console.log(chalk_1.default.gray(` Repo: ${app.repository} (${app.repositoryBranch || 'main'})`));
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
console.log('');
|
|
3167
|
-
}
|
|
3168
|
-
catch (e) {
|
|
3169
|
-
console.error(chalk_1.default.red(` ✗ Failed to list apps: ${e.message}`));
|
|
3170
|
-
process.exit(1);
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
// ============================================================================
|
|
3174
|
-
// Query Command
|
|
3175
|
-
// ============================================================================
|
|
3176
|
-
async function runQuery(queryArg, variables) {
|
|
3177
|
-
if (!queryArg) {
|
|
3178
|
-
console.error(chalk_1.default.red('Error: query argument required (inline GraphQL string or .graphql/.gql file path)'));
|
|
3179
|
-
process.exit(2);
|
|
3180
|
-
}
|
|
3181
|
-
// Resolve query: file path or inline string
|
|
3182
|
-
let query;
|
|
3183
|
-
if (queryArg.endsWith('.graphql') || queryArg.endsWith('.gql')) {
|
|
3184
|
-
if (!fs.existsSync(queryArg)) {
|
|
3185
|
-
console.error(chalk_1.default.red(`Error: file not found: ${queryArg}`));
|
|
3186
|
-
process.exit(2);
|
|
3187
|
-
}
|
|
3188
|
-
query = fs.readFileSync(queryArg, 'utf-8');
|
|
3189
|
-
}
|
|
3190
|
-
else {
|
|
3191
|
-
query = queryArg;
|
|
3192
|
-
}
|
|
3193
|
-
// Parse variables if provided
|
|
3194
|
-
let vars = {};
|
|
3195
|
-
if (variables) {
|
|
3196
|
-
try {
|
|
3197
|
-
vars = JSON.parse(variables);
|
|
3198
|
-
}
|
|
3199
|
-
catch {
|
|
3200
|
-
console.error(chalk_1.default.red('Error: --vars must be valid JSON'));
|
|
3201
|
-
process.exit(2);
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
const session = resolveSession();
|
|
3205
|
-
const data = await graphqlRequest(session.domain, session.access_token, query, vars);
|
|
3206
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3207
|
-
}
|
|
3208
|
-
// ============================================================================
|
|
3209
|
-
// GQL Schema Exploration Command
|
|
3210
|
-
// ============================================================================
|
|
3211
|
-
async function runGql(sub, filter) {
|
|
3212
|
-
if (!sub) {
|
|
3213
|
-
console.error(chalk_1.default.red('Error: subcommand required'));
|
|
3214
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql <queries|mutations|types|type> [name] [--filter <text>]`));
|
|
3215
|
-
process.exit(2);
|
|
3216
|
-
}
|
|
3217
|
-
const session = resolveSession();
|
|
3218
|
-
if (sub === 'type') {
|
|
3219
|
-
if (!filter) {
|
|
3220
|
-
console.error(chalk_1.default.red('Error: type name required'));
|
|
3221
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql type <TypeName>`));
|
|
3222
|
-
process.exit(2);
|
|
3223
|
-
}
|
|
3224
|
-
await runGqlType(session, filter);
|
|
3225
|
-
}
|
|
3226
|
-
else if (sub === 'queries') {
|
|
3227
|
-
await runGqlRootFields(session, 'queryType', filter);
|
|
3228
|
-
}
|
|
3229
|
-
else if (sub === 'mutations') {
|
|
3230
|
-
await runGqlRootFields(session, 'mutationType', filter);
|
|
3231
|
-
}
|
|
3232
|
-
else if (sub === 'types') {
|
|
3233
|
-
await runGqlTypes(session, filter);
|
|
3234
|
-
}
|
|
3235
|
-
else {
|
|
3236
|
-
console.error(chalk_1.default.red(`Unknown gql subcommand: ${sub}`));
|
|
3237
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} gql <queries|mutations|types|type> [--filter <text>]`));
|
|
3238
|
-
process.exit(2);
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
function formatGqlType(t) {
|
|
3242
|
-
if (!t)
|
|
3243
|
-
return 'unknown';
|
|
3244
|
-
if (t.kind === 'NON_NULL')
|
|
3245
|
-
return `${formatGqlType(t.ofType)}!`;
|
|
3246
|
-
if (t.kind === 'LIST')
|
|
3247
|
-
return `[${formatGqlType(t.ofType)}]`;
|
|
3248
|
-
return t.name || 'unknown';
|
|
3249
|
-
}
|
|
3250
|
-
async function runGqlType(session, typeName) {
|
|
3251
|
-
const query = `{
|
|
3252
|
-
__type(name: "${typeName}") {
|
|
3253
|
-
name kind description
|
|
3254
|
-
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 } }
|
|
3255
|
-
inputFields { name type { name kind ofType { name kind ofType { name kind } } } defaultValue }
|
|
3256
|
-
enumValues { name description }
|
|
3257
|
-
}
|
|
3258
|
-
}`;
|
|
3259
|
-
const data = await graphqlRequest(session.domain, session.access_token, query, {});
|
|
3260
|
-
const type = data.__type;
|
|
3261
|
-
if (!type) {
|
|
3262
|
-
console.error(chalk_1.default.red(`Type "${typeName}" not found`));
|
|
3263
|
-
process.exit(1);
|
|
3264
|
-
}
|
|
3265
|
-
console.log(chalk_1.default.bold.cyan(`${type.name}`) + chalk_1.default.gray(` (${type.kind})`));
|
|
3266
|
-
if (type.description)
|
|
3267
|
-
console.log(chalk_1.default.gray(type.description));
|
|
3268
|
-
console.log('');
|
|
3269
|
-
if (type.fields && type.fields.length > 0) {
|
|
3270
|
-
console.log(chalk_1.default.bold.yellow('Fields:'));
|
|
3271
|
-
for (const f of type.fields) {
|
|
3272
|
-
const typeStr = formatGqlType(f.type);
|
|
3273
|
-
let line = ` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(typeStr)}`;
|
|
3274
|
-
if (f.args && f.args.length > 0) {
|
|
3275
|
-
const argsStr = f.args.map((a) => {
|
|
3276
|
-
const argType = formatGqlType(a.type);
|
|
3277
|
-
return a.defaultValue ? `${a.name}: ${argType} = ${a.defaultValue}` : `${a.name}: ${argType}`;
|
|
3278
|
-
}).join(', ');
|
|
3279
|
-
line += chalk_1.default.gray(` (${argsStr})`);
|
|
3280
|
-
}
|
|
3281
|
-
if (f.description)
|
|
3282
|
-
line += chalk_1.default.gray(` — ${f.description}`);
|
|
3283
|
-
console.log(line);
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
if (type.inputFields && type.inputFields.length > 0) {
|
|
3287
|
-
console.log(chalk_1.default.bold.yellow('Input Fields:'));
|
|
3288
|
-
for (const f of type.inputFields) {
|
|
3289
|
-
const typeStr = formatGqlType(f.type);
|
|
3290
|
-
let line = ` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(typeStr)}`;
|
|
3291
|
-
if (f.defaultValue)
|
|
3292
|
-
line += chalk_1.default.gray(` = ${f.defaultValue}`);
|
|
3293
|
-
console.log(line);
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
if (type.enumValues && type.enumValues.length > 0) {
|
|
3297
|
-
console.log(chalk_1.default.bold.yellow('Enum Values:'));
|
|
3298
|
-
for (const v of type.enumValues) {
|
|
3299
|
-
let line = ` ${chalk_1.default.green(v.name)}`;
|
|
3300
|
-
if (v.description)
|
|
3301
|
-
line += chalk_1.default.gray(` — ${v.description}`);
|
|
3302
|
-
console.log(line);
|
|
3303
|
-
}
|
|
3304
|
-
}
|
|
3305
|
-
}
|
|
3306
|
-
async function runGqlRootFields(session, rootType, filter) {
|
|
3307
|
-
const query = `{
|
|
3308
|
-
__schema {
|
|
3309
|
-
${rootType} {
|
|
3310
|
-
fields { name description args { name type { name kind ofType { name kind ofType { name kind } } } defaultValue } type { name kind ofType { name kind ofType { name kind } } } }
|
|
3311
|
-
}
|
|
3312
|
-
}
|
|
3313
|
-
}`;
|
|
3314
|
-
const data = await graphqlRequest(session.domain, session.access_token, query, {});
|
|
3315
|
-
const fields = data.__schema?.[rootType]?.fields || [];
|
|
3316
|
-
const filtered = filter
|
|
3317
|
-
? fields.filter((f) => f.name.toLowerCase().includes(filter.toLowerCase()))
|
|
3318
|
-
: fields;
|
|
3319
|
-
const label = rootType === 'queryType' ? 'Queries' : 'Mutations';
|
|
3320
|
-
console.log(chalk_1.default.bold.yellow(`${label}${filter ? ` (filter: "${filter}")` : ''}:`));
|
|
3321
|
-
console.log('');
|
|
3322
|
-
for (const f of filtered) {
|
|
3323
|
-
const returnType = formatGqlType(f.type);
|
|
3324
|
-
console.log(` ${chalk_1.default.green(f.name)}: ${chalk_1.default.cyan(returnType)}`);
|
|
3325
|
-
if (f.description)
|
|
3326
|
-
console.log(` ${chalk_1.default.gray(f.description)}`);
|
|
3327
|
-
if (f.args && f.args.length > 0) {
|
|
3328
|
-
for (const a of f.args) {
|
|
3329
|
-
const argType = formatGqlType(a.type);
|
|
3330
|
-
const def = a.defaultValue ? chalk_1.default.gray(` = ${a.defaultValue}`) : '';
|
|
3331
|
-
console.log(` ${chalk_1.default.white(a.name)}: ${chalk_1.default.cyan(argType)}${def}`);
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
|
-
console.log('');
|
|
3335
|
-
}
|
|
3336
|
-
console.log(chalk_1.default.gray(`${filtered.length} ${label.toLowerCase()} found`));
|
|
3337
|
-
}
|
|
3338
|
-
async function runGqlTypes(session, filter) {
|
|
3339
|
-
const query = `{
|
|
3340
|
-
__schema {
|
|
3341
|
-
types { name kind description }
|
|
3342
|
-
}
|
|
3343
|
-
}`;
|
|
3344
|
-
const data = await graphqlRequest(session.domain, session.access_token, query, {});
|
|
3345
|
-
const types = (data.__schema?.types || [])
|
|
3346
|
-
.filter((t) => !t.name.startsWith('__'))
|
|
3347
|
-
.filter((t) => !filter || t.name.toLowerCase().includes(filter.toLowerCase()));
|
|
3348
|
-
const grouped = {};
|
|
3349
|
-
for (const t of types) {
|
|
3350
|
-
const kind = t.kind || 'OTHER';
|
|
3351
|
-
if (!grouped[kind])
|
|
3352
|
-
grouped[kind] = [];
|
|
3353
|
-
grouped[kind].push(t);
|
|
3354
|
-
}
|
|
3355
|
-
const kindOrder = ['OBJECT', 'INPUT_OBJECT', 'ENUM', 'INTERFACE', 'UNION', 'SCALAR'];
|
|
3356
|
-
for (const kind of kindOrder) {
|
|
3357
|
-
const items = grouped[kind];
|
|
3358
|
-
if (!items || items.length === 0)
|
|
3359
|
-
continue;
|
|
3360
|
-
console.log(chalk_1.default.bold.yellow(`${kind} (${items.length}):`));
|
|
3361
|
-
for (const t of items.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3362
|
-
let line = ` ${chalk_1.default.green(t.name)}`;
|
|
3363
|
-
if (t.description)
|
|
3364
|
-
line += chalk_1.default.gray(` — ${t.description}`);
|
|
3365
|
-
console.log(line);
|
|
3366
|
-
}
|
|
3367
|
-
console.log('');
|
|
3368
|
-
}
|
|
3369
|
-
console.log(chalk_1.default.gray(`${types.length} types found${filter ? ` matching "${filter}"` : ''}`));
|
|
3370
|
-
}
|
|
3371
|
-
// ============================================================================
|
|
3372
|
-
// Extract Command
|
|
3373
|
-
// ============================================================================
|
|
3374
|
-
function runExtract(sourceFile, componentName, targetFile, copy) {
|
|
3375
|
-
// Validate args
|
|
3376
|
-
if (!sourceFile || !componentName || !targetFile) {
|
|
3377
|
-
console.error(chalk_1.default.red('Error: Missing required arguments'));
|
|
3378
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file> [--copy]`));
|
|
3379
|
-
process.exit(2);
|
|
3380
|
-
}
|
|
3381
|
-
// Check source exists
|
|
3382
|
-
if (!fs.existsSync(sourceFile)) {
|
|
3383
|
-
console.error(chalk_1.default.red(`Error: Source file not found: ${sourceFile}`));
|
|
3384
|
-
process.exit(2);
|
|
3385
|
-
}
|
|
3386
|
-
// Read and parse source (Document API preserves comments)
|
|
3387
|
-
const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
|
|
3388
|
-
const srcDoc = yaml_1.default.parseDocument(sourceContent);
|
|
3389
|
-
const sourceJS = srcDoc.toJS();
|
|
3390
|
-
if (!sourceJS || !Array.isArray(sourceJS.components)) {
|
|
3391
|
-
console.error(chalk_1.default.red(`Error: Source file is not a valid module (missing components array): ${sourceFile}`));
|
|
3392
|
-
process.exit(2);
|
|
3393
|
-
}
|
|
3394
|
-
// Get the AST components sequence
|
|
3395
|
-
const srcComponents = srcDoc.get('components', true);
|
|
3396
|
-
if (!(0, yaml_1.isSeq)(srcComponents)) {
|
|
3397
|
-
console.error(chalk_1.default.red(`Error: Source components is not a sequence: ${sourceFile}`));
|
|
3398
|
-
process.exit(2);
|
|
3399
|
-
}
|
|
3400
|
-
// Find component by exact name match
|
|
3401
|
-
const compIndex = srcComponents.items.findIndex((item) => {
|
|
3402
|
-
return (0, yaml_1.isMap)(item) && item.get('name') === componentName;
|
|
3403
|
-
});
|
|
3404
|
-
if (compIndex === -1) {
|
|
3405
|
-
const available = sourceJS.components.map((c) => c.name).filter(Boolean);
|
|
3406
|
-
console.error(chalk_1.default.red(`Error: Component not found: ${componentName}`));
|
|
3407
|
-
if (available.length > 0) {
|
|
3408
|
-
console.error(chalk_1.default.gray('Available components:'));
|
|
3409
|
-
for (const name of available) {
|
|
3410
|
-
console.error(chalk_1.default.gray(` - ${name}`));
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
process.exit(2);
|
|
3414
|
-
}
|
|
3415
|
-
// Get the component AST node (clone for copy, take for move)
|
|
3416
|
-
const componentNode = copy
|
|
3417
|
-
? srcDoc.createNode(sourceJS.components[compIndex])
|
|
3418
|
-
: srcComponents.items[compIndex];
|
|
3419
|
-
// Capture comment: if this is the first item, the comment lives on the parent seq
|
|
3420
|
-
let componentComment;
|
|
3421
|
-
if (compIndex === 0 && srcComponents.commentBefore) {
|
|
3422
|
-
componentComment = srcComponents.commentBefore;
|
|
3423
|
-
if (!copy) {
|
|
3424
|
-
// Transfer the comment away from the source seq (it belongs to the extracted component)
|
|
3425
|
-
srcComponents.commentBefore = undefined;
|
|
3426
|
-
}
|
|
3427
|
-
}
|
|
3428
|
-
else {
|
|
3429
|
-
componentComment = componentNode.commentBefore;
|
|
3430
|
-
}
|
|
3431
|
-
// Find matching routes (by index in AST)
|
|
3432
|
-
const srcRoutes = srcDoc.get('routes', true);
|
|
3433
|
-
const matchedRouteIndices = [];
|
|
3434
|
-
if ((0, yaml_1.isSeq)(srcRoutes)) {
|
|
3435
|
-
srcRoutes.items.forEach((item, idx) => {
|
|
3436
|
-
if ((0, yaml_1.isMap)(item) && item.get('component') === componentName) {
|
|
3437
|
-
matchedRouteIndices.push(idx);
|
|
3438
|
-
}
|
|
3439
|
-
});
|
|
3440
|
-
}
|
|
3441
|
-
// Collect route AST nodes (clone for copy, reference for move)
|
|
3442
|
-
const routeNodes = matchedRouteIndices.map(idx => {
|
|
3443
|
-
if (copy) {
|
|
3444
|
-
return srcDoc.createNode(sourceJS.routes[idx]);
|
|
3445
|
-
}
|
|
3446
|
-
return srcRoutes.items[idx];
|
|
3447
|
-
});
|
|
3448
|
-
// Load or create target document
|
|
3449
|
-
let tgtDoc;
|
|
3450
|
-
let targetCreated = false;
|
|
3451
|
-
if (fs.existsSync(targetFile)) {
|
|
3452
|
-
const targetContent = fs.readFileSync(targetFile, 'utf-8');
|
|
3453
|
-
tgtDoc = yaml_1.default.parseDocument(targetContent);
|
|
3454
|
-
const targetJS = tgtDoc.toJS();
|
|
3455
|
-
if (!targetJS || !Array.isArray(targetJS.components)) {
|
|
3456
|
-
console.error(chalk_1.default.red(`Error: Target file is not a valid module (missing components array): ${targetFile}`));
|
|
3457
|
-
process.exit(2);
|
|
3458
|
-
}
|
|
3459
|
-
// Check for duplicate component name
|
|
3460
|
-
const duplicate = targetJS.components.find((c) => c.name === componentName);
|
|
3461
|
-
if (duplicate) {
|
|
3462
|
-
console.error(chalk_1.default.red(`Error: Target already contains a component named "${componentName}"`));
|
|
3463
|
-
process.exit(2);
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
else {
|
|
3467
|
-
// Create new module scaffold
|
|
3468
|
-
const baseName = path.basename(targetFile, path.extname(targetFile));
|
|
3469
|
-
const moduleName = baseName
|
|
3470
|
-
.split('-')
|
|
3471
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
3472
|
-
.join('');
|
|
3473
|
-
const sourceModule = typeof sourceJS.module === 'object' ? sourceJS.module : null;
|
|
3474
|
-
const displayName = moduleName.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
3475
|
-
const moduleObj = {
|
|
3476
|
-
name: moduleName,
|
|
3477
|
-
appModuleId: generateUUID(),
|
|
3478
|
-
displayName: { 'en-US': displayName },
|
|
3479
|
-
description: { 'en-US': `${displayName} module` },
|
|
3480
|
-
application: 'System',
|
|
3481
|
-
};
|
|
3482
|
-
// In copy mode, set priority higher than source
|
|
3483
|
-
if (copy) {
|
|
3484
|
-
const sourcePriority = sourceModule?.priority;
|
|
3485
|
-
moduleObj.priority = (0, extractUtils_1.computeExtractPriority)(sourcePriority);
|
|
3486
|
-
}
|
|
3487
|
-
// Parse from string so the document has proper AST context for comment preservation
|
|
3488
|
-
const scaffoldStr = yaml_1.default.stringify({
|
|
3489
|
-
module: moduleObj,
|
|
3490
|
-
entities: [],
|
|
3491
|
-
permissions: [],
|
|
3492
|
-
components: [],
|
|
3493
|
-
routes: []
|
|
3494
|
-
}, { indent: 2, lineWidth: 0, singleQuote: false });
|
|
3495
|
-
tgtDoc = yaml_1.default.parseDocument(scaffoldStr);
|
|
3496
|
-
targetCreated = true;
|
|
3497
|
-
}
|
|
3498
|
-
// Add component to target (ensure block style so comments are preserved)
|
|
3499
|
-
const tgtComponents = tgtDoc.get('components', true);
|
|
3500
|
-
if ((0, yaml_1.isSeq)(tgtComponents)) {
|
|
3501
|
-
tgtComponents.flow = false;
|
|
3502
|
-
// Apply the captured comment: if it's the first item in target, set on seq; otherwise on node
|
|
3503
|
-
if (componentComment) {
|
|
3504
|
-
if (tgtComponents.items.length === 0) {
|
|
3505
|
-
tgtComponents.commentBefore = componentComment;
|
|
3506
|
-
}
|
|
3507
|
-
else {
|
|
3508
|
-
componentNode.commentBefore = componentComment;
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
tgtComponents.items.push(componentNode);
|
|
3512
|
-
}
|
|
3513
|
-
else {
|
|
3514
|
-
tgtDoc.addIn(['components'], componentNode);
|
|
1474
|
+
tgtDoc.addIn(['components'], componentNode);
|
|
3515
1475
|
}
|
|
3516
1476
|
// In move mode, remove component from source
|
|
3517
1477
|
if (!copy) {
|
|
@@ -3579,7 +1539,7 @@ function parseArgs(args) {
|
|
|
3579
1539
|
reportFormat: 'json'
|
|
3580
1540
|
};
|
|
3581
1541
|
// Check for commands
|
|
3582
|
-
const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude'
|
|
1542
|
+
const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude'];
|
|
3583
1543
|
if (args.length > 0 && commands.includes(args[0])) {
|
|
3584
1544
|
command = args[0];
|
|
3585
1545
|
args = args.slice(1);
|
|
@@ -3655,50 +1615,6 @@ function parseArgs(args) {
|
|
|
3655
1615
|
else if (arg === '--copy') {
|
|
3656
1616
|
options.extractCopy = true;
|
|
3657
1617
|
}
|
|
3658
|
-
else if (arg === '--org') {
|
|
3659
|
-
const orgArg = args[++i];
|
|
3660
|
-
const parsed = parseInt(orgArg, 10);
|
|
3661
|
-
if (isNaN(parsed)) {
|
|
3662
|
-
console.error(chalk_1.default.red(`Invalid --org value: ${orgArg}. Must be a number.`));
|
|
3663
|
-
process.exit(2);
|
|
3664
|
-
}
|
|
3665
|
-
options.orgId = parsed;
|
|
3666
|
-
}
|
|
3667
|
-
else if (arg === '--vars') {
|
|
3668
|
-
options.vars = args[++i];
|
|
3669
|
-
}
|
|
3670
|
-
else if (arg === '--from') {
|
|
3671
|
-
options.from = args[++i];
|
|
3672
|
-
}
|
|
3673
|
-
else if (arg === '--to') {
|
|
3674
|
-
options.to = args[++i];
|
|
3675
|
-
}
|
|
3676
|
-
else if (arg === '--output' || arg === '-o') {
|
|
3677
|
-
options.output = args[++i];
|
|
3678
|
-
}
|
|
3679
|
-
else if (arg === '--console') {
|
|
3680
|
-
options.console = true;
|
|
3681
|
-
}
|
|
3682
|
-
else if (arg === '--message' || arg === '-m') {
|
|
3683
|
-
options.message = args[++i];
|
|
3684
|
-
}
|
|
3685
|
-
else if (arg === '--branch' || arg === '-b') {
|
|
3686
|
-
options.branch = args[++i];
|
|
3687
|
-
}
|
|
3688
|
-
else if (arg === '--file') {
|
|
3689
|
-
if (!options.file)
|
|
3690
|
-
options.file = [];
|
|
3691
|
-
options.file.push(args[++i]);
|
|
3692
|
-
}
|
|
3693
|
-
else if (arg === '--filter') {
|
|
3694
|
-
options.filter = args[++i];
|
|
3695
|
-
}
|
|
3696
|
-
else if (arg === '--force') {
|
|
3697
|
-
options.force = true;
|
|
3698
|
-
}
|
|
3699
|
-
else if (arg === '--skip-changed') {
|
|
3700
|
-
options.skipChanged = true;
|
|
3701
|
-
}
|
|
3702
1618
|
else if (!arg.startsWith('-')) {
|
|
3703
1619
|
files.push(arg);
|
|
3704
1620
|
}
|
|
@@ -4105,13 +2021,13 @@ function getSuggestion(error) {
|
|
|
4105
2021
|
return 'Check that the value type matches the expected type (string, number, boolean, etc.)';
|
|
4106
2022
|
}
|
|
4107
2023
|
if (error.message.includes('additionalProperties')) {
|
|
4108
|
-
return 'Remove unrecognized properties. Use `
|
|
2024
|
+
return 'Remove unrecognized properties. Use `cx-cli schema <type>` to see allowed properties.';
|
|
4109
2025
|
}
|
|
4110
2026
|
return 'Review the schema requirements for this property';
|
|
4111
2027
|
case 'yaml_syntax_error':
|
|
4112
2028
|
return 'Check YAML indentation and syntax. Use a YAML linter to identify issues.';
|
|
4113
2029
|
case 'invalid_task_type':
|
|
4114
|
-
return `Use '
|
|
2030
|
+
return `Use 'cx-cli list --type workflow' to see available task types`;
|
|
4115
2031
|
case 'invalid_activity':
|
|
4116
2032
|
return 'Each activity must have a "name" and "steps" array';
|
|
4117
2033
|
default:
|
|
@@ -4485,151 +2401,7 @@ async function main() {
|
|
|
4485
2401
|
}
|
|
4486
2402
|
// Handle version
|
|
4487
2403
|
if (options.version) {
|
|
4488
|
-
console.log(`
|
|
4489
|
-
process.exit(0);
|
|
4490
|
-
}
|
|
4491
|
-
// Handle login command (no schemas needed)
|
|
4492
|
-
if (command === 'login') {
|
|
4493
|
-
if (!files[0]) {
|
|
4494
|
-
console.error(chalk_1.default.red('Error: URL required'));
|
|
4495
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} login <url>`));
|
|
4496
|
-
process.exit(2);
|
|
4497
|
-
}
|
|
4498
|
-
await runLogin(files[0]);
|
|
4499
|
-
process.exit(0);
|
|
4500
|
-
}
|
|
4501
|
-
// Handle logout command (no schemas needed)
|
|
4502
|
-
if (command === 'logout') {
|
|
4503
|
-
await runLogout(files[0]);
|
|
4504
|
-
process.exit(0);
|
|
4505
|
-
}
|
|
4506
|
-
// Handle pat command (no schemas needed)
|
|
4507
|
-
if (command === 'pat') {
|
|
4508
|
-
const sub = files[0];
|
|
4509
|
-
if (sub === 'create') {
|
|
4510
|
-
if (!files[1]) {
|
|
4511
|
-
console.error(chalk_1.default.red('Error: Token name required'));
|
|
4512
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat create <name>`));
|
|
4513
|
-
process.exit(2);
|
|
4514
|
-
}
|
|
4515
|
-
await runPatCreate(files[1]);
|
|
4516
|
-
}
|
|
4517
|
-
else if (sub === 'list' || !sub) {
|
|
4518
|
-
await runPatList();
|
|
4519
|
-
}
|
|
4520
|
-
else if (sub === 'revoke') {
|
|
4521
|
-
if (!files[1]) {
|
|
4522
|
-
console.error(chalk_1.default.red('Error: Token ID required'));
|
|
4523
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat revoke <tokenId>`));
|
|
4524
|
-
process.exit(2);
|
|
4525
|
-
}
|
|
4526
|
-
await runPatRevoke(files[1]);
|
|
4527
|
-
}
|
|
4528
|
-
else if (sub === 'setup') {
|
|
4529
|
-
await runPatSetup();
|
|
4530
|
-
}
|
|
4531
|
-
else {
|
|
4532
|
-
console.error(chalk_1.default.red(`Unknown pat subcommand: ${sub}`));
|
|
4533
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat <create|list|revoke|setup>`));
|
|
4534
|
-
process.exit(2);
|
|
4535
|
-
}
|
|
4536
|
-
process.exit(0);
|
|
4537
|
-
}
|
|
4538
|
-
// Handle orgs command (no schemas needed)
|
|
4539
|
-
if (command === 'orgs') {
|
|
4540
|
-
const sub = files[0];
|
|
4541
|
-
if (sub === 'list' || !sub) {
|
|
4542
|
-
await runOrgsList();
|
|
4543
|
-
}
|
|
4544
|
-
else if (sub === 'use') {
|
|
4545
|
-
await runOrgsUse(files[1]);
|
|
4546
|
-
}
|
|
4547
|
-
else if (sub === 'select') {
|
|
4548
|
-
await runOrgsSelect();
|
|
4549
|
-
}
|
|
4550
|
-
else {
|
|
4551
|
-
console.error(chalk_1.default.red(`Unknown orgs subcommand: ${sub}`));
|
|
4552
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} orgs <list|use|select>`));
|
|
4553
|
-
process.exit(2);
|
|
4554
|
-
}
|
|
4555
|
-
process.exit(0);
|
|
4556
|
-
}
|
|
4557
|
-
// Handle appmodule command (no schemas needed)
|
|
4558
|
-
if (command === 'appmodule') {
|
|
4559
|
-
const sub = files[0];
|
|
4560
|
-
if (sub === 'deploy') {
|
|
4561
|
-
await runAppModuleDeploy(files[1], options.orgId);
|
|
4562
|
-
}
|
|
4563
|
-
else if (sub === 'undeploy') {
|
|
4564
|
-
await runAppModuleUndeploy(files[1], options.orgId);
|
|
4565
|
-
}
|
|
4566
|
-
else {
|
|
4567
|
-
console.error(chalk_1.default.red(`Unknown appmodule subcommand: ${sub || '(none)'}`));
|
|
4568
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule <deploy|undeploy> ...`));
|
|
4569
|
-
process.exit(2);
|
|
4570
|
-
}
|
|
4571
|
-
process.exit(0);
|
|
4572
|
-
}
|
|
4573
|
-
// Handle workflow command (no schemas needed)
|
|
4574
|
-
if (command === 'workflow') {
|
|
4575
|
-
const sub = files[0];
|
|
4576
|
-
if (sub === 'deploy') {
|
|
4577
|
-
await runWorkflowDeploy(files[1], options.orgId);
|
|
4578
|
-
}
|
|
4579
|
-
else if (sub === 'undeploy') {
|
|
4580
|
-
await runWorkflowUndeploy(files[1], options.orgId);
|
|
4581
|
-
}
|
|
4582
|
-
else if (sub === 'execute') {
|
|
4583
|
-
await runWorkflowExecute(files[1], options.orgId, options.vars, options.file);
|
|
4584
|
-
}
|
|
4585
|
-
else if (sub === 'logs') {
|
|
4586
|
-
await runWorkflowLogs(files[1], options.orgId, options.from, options.to);
|
|
4587
|
-
}
|
|
4588
|
-
else if (sub === 'log') {
|
|
4589
|
-
await runWorkflowLog(files[1], options.orgId, options.output, options.console, options.format === 'json');
|
|
4590
|
-
}
|
|
4591
|
-
else {
|
|
4592
|
-
console.error(chalk_1.default.red(`Unknown workflow subcommand: ${sub || '(none)'}`));
|
|
4593
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <deploy|undeploy|execute|logs|log> ...`));
|
|
4594
|
-
process.exit(2);
|
|
4595
|
-
}
|
|
4596
|
-
process.exit(0);
|
|
4597
|
-
}
|
|
4598
|
-
// Handle publish command (no schemas needed)
|
|
4599
|
-
if (command === 'publish') {
|
|
4600
|
-
await runPublish(files[0] || options.feature, options.orgId);
|
|
4601
|
-
process.exit(0);
|
|
4602
|
-
}
|
|
4603
|
-
// Handle app command (no schemas needed)
|
|
4604
|
-
if (command === 'app') {
|
|
4605
|
-
const sub = files[0];
|
|
4606
|
-
if (sub === 'install' || sub === 'upgrade') {
|
|
4607
|
-
await runAppInstall(options.orgId, options.branch, options.force, options.skipChanged);
|
|
4608
|
-
}
|
|
4609
|
-
else if (sub === 'release' || sub === 'publish') {
|
|
4610
|
-
await runAppPublish(options.orgId, options.message, options.branch, options.force, files.slice(1));
|
|
4611
|
-
}
|
|
4612
|
-
else if (sub === 'list' || !sub) {
|
|
4613
|
-
await runAppList(options.orgId);
|
|
4614
|
-
}
|
|
4615
|
-
else {
|
|
4616
|
-
console.error(chalk_1.default.red(`Unknown app subcommand: ${sub}`));
|
|
4617
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} app <install|upgrade|release|list>`));
|
|
4618
|
-
process.exit(2);
|
|
4619
|
-
}
|
|
4620
|
-
process.exit(0);
|
|
4621
|
-
}
|
|
4622
|
-
// Handle gql command (no schemas needed)
|
|
4623
|
-
if (command === 'gql') {
|
|
4624
|
-
const sub = files[0];
|
|
4625
|
-
// For 'gql type <name>', the type name is in files[1] — use it as filter
|
|
4626
|
-
const filterArg = sub === 'type' ? (files[1] || options.filter) : options.filter;
|
|
4627
|
-
await runGql(sub, filterArg);
|
|
4628
|
-
process.exit(0);
|
|
4629
|
-
}
|
|
4630
|
-
// Handle query command (no schemas needed)
|
|
4631
|
-
if (command === 'query') {
|
|
4632
|
-
await runQuery(files[0], options.vars);
|
|
2404
|
+
console.log(`cx-cli v${VERSION}`);
|
|
4633
2405
|
process.exit(0);
|
|
4634
2406
|
}
|
|
4635
2407
|
// Find schemas path
|