@cxtms/cx-schema 1.7.17 → 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.
Files changed (78) hide show
  1. package/.claude/skills/cx-core/SKILL.md +38 -20
  2. package/.claude/skills/cx-core/ref-entity-accounting.md +0 -7
  3. package/.claude/skills/cx-core/ref-entity-commodity.md +0 -12
  4. package/.claude/skills/cx-core/ref-entity-contact.md +0 -10
  5. package/.claude/skills/cx-core/ref-entity-geography.md +0 -9
  6. package/.claude/skills/cx-core/ref-entity-notification.md +85 -0
  7. package/.claude/skills/cx-core/ref-entity-order-sub.md +7 -7
  8. package/.claude/skills/cx-core/ref-entity-order.md +1 -14
  9. package/.claude/skills/cx-core/ref-entity-rate.md +0 -8
  10. package/.claude/skills/cx-core/ref-entity-shared.md +20 -9
  11. package/.claude/skills/cx-core/ref-entity-warehouse.md +0 -5
  12. package/.claude/skills/cx-module/SKILL.md +28 -103
  13. package/.claude/skills/cx-module/ref-components-data.md +0 -7
  14. package/.claude/skills/cx-module/ref-components-display.md +0 -13
  15. package/.claude/skills/cx-module/ref-components-forms.md +1 -6
  16. package/.claude/skills/cx-module/ref-components-interactive.md +0 -11
  17. package/.claude/skills/cx-module/ref-components-layout.md +0 -11
  18. package/.claude/skills/cx-module/ref-components-specialized.md +0 -50
  19. package/.claude/skills/cx-workflow/SKILL.md +21 -130
  20. package/.claude/skills/cx-workflow/ref-communication.md +0 -8
  21. package/.claude/skills/cx-workflow/ref-entity.md +62 -17
  22. package/.claude/skills/cx-workflow/ref-expressions.md +272 -0
  23. package/.claude/skills/cx-workflow/ref-flow.md +5 -11
  24. package/.claude/skills/cx-workflow/ref-other.md +33 -14
  25. package/.claude/skills/cx-workflow/ref-utilities.md +111 -14
  26. package/README.md +34 -34
  27. package/dist/cli.js +166 -2394
  28. package/dist/cli.js.map +1 -1
  29. package/dist/types.d.ts +0 -2
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/validator.d.ts +0 -8
  32. package/dist/validator.d.ts.map +1 -1
  33. package/dist/validator.js +2 -54
  34. package/dist/validator.js.map +1 -1
  35. package/dist/workflowValidator.d.ts +0 -4
  36. package/dist/workflowValidator.d.ts.map +1 -1
  37. package/dist/workflowValidator.js +2 -28
  38. package/dist/workflowValidator.js.map +1 -1
  39. package/package.json +2 -3
  40. package/schemas/actions/all.json +2 -1
  41. package/schemas/actions/clipboard.json +46 -0
  42. package/schemas/components/appComponent.json +0 -8
  43. package/schemas/components/module.json +2 -31
  44. package/schemas/components/timelineGrid.json +0 -4
  45. package/schemas/schemas.json +0 -12
  46. package/schemas/workflows/flow/entity.json +19 -0
  47. package/schemas/workflows/tasks/all.json +9 -0
  48. package/schemas/workflows/tasks/authentication.json +12 -26
  49. package/schemas/workflows/tasks/edi.json +93 -1
  50. package/schemas/workflows/tasks/httpRequest.json +4 -0
  51. package/schemas/workflows/tasks/order.json +45 -0
  52. package/schemas/workflows/tasks/resolve-timezone.json +65 -0
  53. package/schemas/workflows/tasks/transmission.json +185 -0
  54. package/schemas/workflows/tasks/unzip-file.json +68 -0
  55. package/schemas/workflows/variable.json +5 -1
  56. package/schemas/workflows/workflow.json +4 -0
  57. package/scripts/postinstall.js +1 -1
  58. package/templates/module-configuration.yaml +89 -23
  59. package/templates/module-form.yaml +3 -3
  60. package/templates/module-grid.yaml +3 -3
  61. package/templates/module-select.yaml +3 -3
  62. package/templates/module.yaml +2 -3
  63. package/templates/workflow-api-tracking.yaml +1 -1
  64. package/templates/workflow-basic.yaml +1 -1
  65. package/templates/workflow-document.yaml +1 -1
  66. package/templates/workflow-entity-trigger.yaml +1 -1
  67. package/templates/workflow-ftp-edi.yaml +1 -1
  68. package/templates/workflow-ftp-tracking.yaml +1 -1
  69. package/templates/workflow-mcp-tool.yaml +1 -1
  70. package/templates/workflow-public-api.yaml +1 -1
  71. package/templates/workflow-scheduled.yaml +1 -1
  72. package/templates/workflow-utility.yaml +1 -1
  73. package/templates/workflow-webhook.yaml +1 -1
  74. package/templates/workflow.yaml +1 -1
  75. package/.claude/skills/cx-core/ref-cli-auth.md +0 -120
  76. package/.claude/skills/cx-core/ref-graphql-query.md +0 -320
  77. package/.claude/skills/cx-workflow/ref-expressions-ncalc.md +0 -111
  78. package/.claude/skills/cx-workflow/ref-expressions-template.md +0 -148
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 = 'cxtms';
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 `id: "${generateUUID()}"
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 cxtms modules/*.yaml
370
+ npx cx-cli modules/*.yaml
556
371
 
557
372
  # Validate all workflows
558
- npx cxtms workflows/*.yaml
373
+ npx cx-cli workflows/*.yaml
559
374
 
560
375
  # Validate with detailed output
561
- npx cxtms --verbose modules/my-module.yaml
376
+ npx cx-cli --verbose modules/my-module.yaml
562
377
 
563
378
  # Generate validation report
564
- npx cxtms report modules/*.yaml workflows/*.yaml --report report.html
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 cxtms create module my-module
386
+ npx cx-cli create module my-module
572
387
 
573
388
  # Create a new workflow
574
- npx cxtms create workflow my-workflow
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 cxtms list
396
+ npx cx-cli list
582
397
 
583
398
  # View schema for a component
584
- npx cxtms schema form
399
+ npx cx-cli schema form
585
400
 
586
401
  # View example YAML
587
- npx cxtms example workflow
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 cxtms modules/<module-name>.yaml
423
+ npx cx-cli modules/<module-name>.yaml
609
424
 
610
425
  # Validate a specific workflow file
611
- npx cxtms workflows/<workflow-name>.yaml
426
+ npx cx-cli workflows/<workflow-name>.yaml
612
427
 
613
428
  # Validate all files with a report
614
- npx cxtms report modules/*.yaml workflows/*.yaml --report validation-report.md
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 cxtms schema form
624
- npx cxtms schema dataGrid
625
- npx cxtms schema layout
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 cxtms schema foreach
629
- npx cxtms schema graphql
630
- npx cxtms schema switch
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 cxtms create module <name>
454
+ npx cx-cli create module <name>
640
455
 
641
456
  # Create a new workflow
642
- npx cxtms create workflow <name>
457
+ npx cx-cli create workflow <name>
643
458
 
644
459
  # Create from a specific template variant
645
- npx cxtms create workflow <name> --template basic
460
+ npx cx-cli create workflow <name> --template basic
646
461
 
647
462
  # Create inside a feature folder (features/<name>/workflows/)
648
- npx cxtms create workflow <name> --feature billing
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 configuration templates (fields go directly into layout children)
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(`✓ @cxtms/cx-schema updated to v${installedVersion}`));
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 — \`cxtms\`
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 cxtms create module <name>\` | Scaffold a UI module |
1499
- | \`npx cxtms create workflow <name>\` | Scaffold a workflow |
1500
- | \`npx cxtms create module <name> --template <t>\` | Use a specific template |
1501
- | \`npx cxtms create workflow <name> --template <t>\` | Use a specific template |
1502
- | \`npx cxtms create module <name> --feature <f>\` | Place under features/<f>/modules/ |
1503
- | \`npx cxtms <file.yaml>\` | Validate a YAML file |
1504
- | \`npx cxtms <file.yaml> --verbose\` | Validate with detailed errors |
1505
- | \`npx cxtms schema <name>\` | Show JSON schema for a component or task |
1506
- | \`npx cxtms example <name>\` | Show example YAML |
1507
- | \`npx cxtms list\` | List all available schemas |
1508
- | \`npx cxtms extract <src> <comp> --to <tgt>\` | Move component between modules |
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 cxtms create module|workflow <name> --template <t>\`
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 cxtms <file.yaml>\` — run after every change, fix all errors
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
- // Auth (Login / Logout)
1332
+ // Extract Command
1562
1333
  // ============================================================================
1563
- const AUTH_CALLBACK_PORT = 9000;
1564
- const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
1565
- function getSessionDir() {
1566
- const projectName = path.basename(process.cwd());
1567
- return path.join(os.homedir(), '.cxtms', projectName);
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
- catch {
1580
- return null;
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
- function writeSessionFile(data) {
1584
- const dir = getSessionDir();
1585
- if (!fs.existsSync(dir)) {
1586
- fs.mkdirSync(dir, { recursive: true });
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
- fs.writeFileSync(getSessionFilePath(), JSON.stringify(data, null, 2), 'utf-8');
1589
- }
1590
- function deleteSessionFile() {
1591
- const filePath = getSessionFilePath();
1592
- if (fs.existsSync(filePath)) {
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
- function generateCodeVerifier() {
1597
- return crypto.randomBytes(32).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
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
- function openBrowser(url) {
1627
- const { exec } = require('child_process');
1628
- const cmd = process.platform === 'win32' ? `start "" "${url}"`
1629
- : process.platform === 'darwin' ? `open "${url}"`
1630
- : `xdg-open "${url}"`;
1631
- exec(cmd);
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
- console.log(chalk_1.default.bold.cyan('\n CX CLI Login\n'));
1748
- // Step 1: Register client
1749
- console.log(chalk_1.default.gray(' Registering OAuth client...'));
1750
- const clientId = await registerOAuthClient(domain);
1751
- console.log(chalk_1.default.green(' ✓ Client registered'));
1752
- // Step 2: PKCE
1753
- const codeVerifier = generateCodeVerifier();
1754
- const codeChallenge = generateCodeChallenge(codeVerifier);
1755
- // Step 3: Start callback server
1756
- const callbackPromise = startCallbackServer();
1757
- // Step 4: Open browser
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
- // Try to parse GraphQL errors from 400 responses too
1823
- let json;
1824
- try {
1825
- json = JSON.parse(res.body);
1388
+ else {
1389
+ componentComment = componentNode.commentBefore;
1826
1390
  }
1827
- catch {
1828
- if (res.statusCode !== 200) {
1829
- throw new Error(`GraphQL request failed (${res.statusCode}): ${res.body}`);
1830
- }
1831
- throw new Error('Invalid JSON response from GraphQL endpoint');
1832
- }
1833
- if (json.errors && json.errors.length > 0) {
1834
- const messages = json.errors.map((e) => {
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
- if (res.statusCode !== 200) {
1846
- throw new Error(`GraphQL request failed (${res.statusCode}): ${res.body}`);
1847
- }
1848
- return json.data;
1849
- }
1850
- function graphqlPostWithAuth(domain, token, body) {
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
- function resolveDomainFromAppYaml() {
1877
- const appYamlPath = path.join(process.cwd(), 'app.yaml');
1878
- if (!fs.existsSync(appYamlPath))
1879
- return null;
1880
- const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
1881
- const serverDomain = appYaml?.server || appYaml?.domain;
1882
- if (!serverDomain)
1883
- return null;
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
- return {
1901
- domain,
1902
- client_id: '',
1903
- access_token: patToken,
1904
- refresh_token: '',
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
- const session = resolveSession();
1960
- const domain = session.domain;
1961
- const token = session.access_token;
1962
- const orgId = await resolveOrgId(domain, token, orgOverride);
1963
- // Read and parse YAML
1964
- const yamlContent = fs.readFileSync(file, 'utf-8');
1965
- const parsed = yaml_1.default.parse(yamlContent);
1966
- const appModuleId = parsed?.module?.appModuleId;
1967
- if (!appModuleId) {
1968
- console.error(chalk_1.default.red('Error: Module YAML is missing module.appModuleId'));
1969
- process.exit(2);
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
- // Read app.yaml for appManifestId
1972
- let appManifestId;
1973
- const appYamlPath = path.join(process.cwd(), 'app.yaml');
1974
- if (fs.existsSync(appYamlPath)) {
1975
- const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
1976
- appManifestId = appYaml?.id;
1977
- }
1978
- console.log(chalk_1.default.bold.cyan('\n AppModule Deploy\n'));
1979
- console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
1980
- console.log(chalk_1.default.gray(` Org: ${orgId}`));
1981
- console.log(chalk_1.default.gray(` Module: ${appModuleId}`));
1982
- console.log('');
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
- // Create
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', 'login', 'logout', 'pat', 'appmodule', 'orgs', 'workflow', 'publish', 'query', 'gql', 'app'];
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 `cxtms schema <type>` to see allowed properties.';
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 'cxtms list --type workflow' to see available task types`;
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(`cxtms v${VERSION}`);
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