@cxtms/cx-schema 1.8.1 → 1.8.2

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 (64) hide show
  1. package/.claude/skills/cx-core/ref-cli-auth.md +120 -0
  2. package/.claude/skills/cx-core/ref-entity-accounting.md +7 -0
  3. package/.claude/skills/cx-core/ref-entity-commodity.md +12 -0
  4. package/.claude/skills/cx-core/ref-entity-contact.md +10 -0
  5. package/.claude/skills/cx-core/ref-entity-geography.md +34 -1
  6. package/.claude/skills/cx-core/ref-entity-order-sub.md +13 -0
  7. package/.claude/skills/cx-core/ref-entity-order.md +14 -0
  8. package/.claude/skills/cx-core/ref-entity-rate.md +8 -0
  9. package/.claude/skills/cx-core/ref-entity-shared.md +9 -0
  10. package/.claude/skills/cx-core/ref-entity-warehouse.md +5 -0
  11. package/.claude/skills/cx-core/ref-graphql-query.md +320 -0
  12. package/.claude/skills/cx-module/SKILL.md +103 -28
  13. package/.claude/skills/cx-module/ref-components-data.md +7 -0
  14. package/.claude/skills/cx-module/ref-components-display.md +17 -0
  15. package/.claude/skills/cx-module/ref-components-forms.md +7 -1
  16. package/.claude/skills/cx-module/ref-components-interactive.md +11 -0
  17. package/.claude/skills/cx-module/ref-components-layout.md +11 -0
  18. package/.claude/skills/cx-module/ref-components-specialized.md +50 -0
  19. package/.claude/skills/cx-workflow/SKILL.md +127 -18
  20. package/.claude/skills/cx-workflow/ref-communication.md +8 -0
  21. package/.claude/skills/cx-workflow/ref-entity.md +43 -0
  22. package/.claude/skills/cx-workflow/ref-expressions-ncalc.md +128 -0
  23. package/.claude/skills/cx-workflow/ref-expressions-template.md +159 -0
  24. package/.claude/skills/cx-workflow/ref-flow.md +8 -1
  25. package/.claude/skills/cx-workflow/ref-other.md +9 -0
  26. package/README.md +34 -34
  27. package/dist/cli.js +2395 -167
  28. package/dist/cli.js.map +1 -1
  29. package/dist/types.d.ts +2 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/validator.d.ts +8 -0
  32. package/dist/validator.d.ts.map +1 -1
  33. package/dist/validator.js +54 -2
  34. package/dist/validator.js.map +1 -1
  35. package/dist/workflowValidator.d.ts +4 -0
  36. package/dist/workflowValidator.d.ts.map +1 -1
  37. package/dist/workflowValidator.js +28 -2
  38. package/dist/workflowValidator.js.map +1 -1
  39. package/package.json +1 -1
  40. package/schemas/components/appComponent.json +8 -0
  41. package/schemas/components/module.json +31 -2
  42. package/schemas/components/timelineGrid.json +4 -0
  43. package/schemas/schemas.json +12 -0
  44. package/schemas/workflows/tasks/authentication.json +26 -12
  45. package/schemas/workflows/workflow.json +0 -4
  46. package/scripts/postinstall.js +1 -1
  47. package/templates/module-configuration.yaml +23 -89
  48. package/templates/module-form.yaml +3 -3
  49. package/templates/module-grid.yaml +3 -3
  50. package/templates/module-select.yaml +3 -3
  51. package/templates/module.yaml +3 -2
  52. package/templates/workflow-api-tracking.yaml +1 -1
  53. package/templates/workflow-basic.yaml +1 -1
  54. package/templates/workflow-document.yaml +1 -1
  55. package/templates/workflow-entity-trigger.yaml +1 -1
  56. package/templates/workflow-ftp-edi.yaml +1 -1
  57. package/templates/workflow-ftp-tracking.yaml +1 -1
  58. package/templates/workflow-mcp-tool.yaml +1 -1
  59. package/templates/workflow-public-api.yaml +1 -1
  60. package/templates/workflow-scheduled.yaml +1 -1
  61. package/templates/workflow-utility.yaml +1 -1
  62. package/templates/workflow-webhook.yaml +1 -1
  63. package/templates/workflow.yaml +1 -1
  64. package/.claude/skills/cx-workflow/ref-expressions.md +0 -272
package/dist/cli.js CHANGED
@@ -42,16 +42,47 @@ 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"));
45
49
  const chalk_1 = __importDefault(require("chalk"));
46
50
  const yaml_1 = __importStar(require("yaml"));
47
51
  const validator_1 = require("./validator");
48
52
  const workflowValidator_1 = require("./workflowValidator");
49
53
  const extractUtils_1 = require("./extractUtils");
50
54
  // ============================================================================
55
+ // .env loader — load KEY=VALUE pairs from .env in CWD into process.env
56
+ // ============================================================================
57
+ function loadEnvFile() {
58
+ const envPath = path.join(process.cwd(), '.env');
59
+ if (!fs.existsSync(envPath))
60
+ return;
61
+ const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed || trimmed.startsWith('#'))
65
+ continue;
66
+ const eqIdx = trimmed.indexOf('=');
67
+ if (eqIdx < 1)
68
+ continue;
69
+ const key = trimmed.slice(0, eqIdx).trim();
70
+ let value = trimmed.slice(eqIdx + 1).trim();
71
+ // Strip surrounding quotes
72
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
73
+ value = value.slice(1, -1);
74
+ }
75
+ if (!process.env[key]) {
76
+ process.env[key] = value;
77
+ }
78
+ }
79
+ }
80
+ loadEnvFile();
81
+ // ============================================================================
51
82
  // Constants
52
83
  // ============================================================================
53
84
  const VERSION = require('../package.json').version;
54
- const PROGRAM_NAME = 'cx-cli';
85
+ const PROGRAM_NAME = 'cxtms';
55
86
  // ============================================================================
56
87
  // Help Text
57
88
  // ============================================================================
@@ -79,6 +110,16 @@ ${chalk_1.default.bold.yellow('COMMANDS:')}
79
110
  ${chalk_1.default.green('install-skills')} Install Claude Code skills into project .claude/skills/
80
111
  ${chalk_1.default.green('setup-claude')} Add CX project instructions to CLAUDE.md
81
112
  ${chalk_1.default.green('update')} Update @cxtms/cx-schema to the latest version
113
+ ${chalk_1.default.green('login')} Login to a CX environment (OAuth2 + PKCE)
114
+ ${chalk_1.default.green('logout')} Logout from a CX environment
115
+ ${chalk_1.default.green('pat')} Manage personal access tokens (create, list, revoke)
116
+ ${chalk_1.default.green('orgs')} List, select, or set active organization
117
+ ${chalk_1.default.green('appmodule')} Manage app modules on a CX server (deploy, undeploy)
118
+ ${chalk_1.default.green('workflow')} Manage workflows on a CX server (deploy, undeploy, execute, logs, log)
119
+ ${chalk_1.default.green('publish')} Publish all modules and workflows to a CX server
120
+ ${chalk_1.default.green('app')} Manage app manifests (install/upgrade from git, release to git, list)
121
+ ${chalk_1.default.green('query')} Run a GraphQL query against the CX server
122
+ ${chalk_1.default.green('gql')} Explore GraphQL schema (types, queries, mutations)
82
123
  ${chalk_1.default.green('schema')} Show JSON schema for a component or task
83
124
  ${chalk_1.default.green('example')} Show example YAML for a component or task
84
125
  ${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
@@ -101,6 +142,17 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
101
142
  ${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
102
143
  ${chalk_1.default.green('--to <file>')} Target file for extract command
103
144
  ${chalk_1.default.green('--copy')} Copy component instead of moving (source unchanged, target gets higher priority)
145
+ ${chalk_1.default.green('--org <id>')} Organization ID for server commands
146
+ ${chalk_1.default.green('--vars <json>')} JSON variables for workflow execute
147
+ ${chalk_1.default.green('--from <date>')} Filter logs from date (YYYY-MM-DD)
148
+ ${chalk_1.default.green('--to <date>')} Filter logs to date (YYYY-MM-DD)
149
+ ${chalk_1.default.green('--output <file>')} Save workflow log to file (or -o)
150
+ ${chalk_1.default.green('--console')} Print workflow log to stdout
151
+ ${chalk_1.default.green('--json')} Download JSON log instead of text
152
+ ${chalk_1.default.green('-m, --message <msg>')} Release message for app release (required)
153
+ ${chalk_1.default.green('-b, --branch <branch>')} Branch override for app install/publish
154
+ ${chalk_1.default.green('--force')} Force install (even if same version) or publish all
155
+ ${chalk_1.default.green('--skip-changed')} Skip modules with unpublished changes during install
104
156
 
105
157
  ${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
106
158
  ${chalk_1.default.gray('# Validate a module YAML file')}
@@ -166,6 +218,136 @@ ${chalk_1.default.bold.yellow('SCHEMA COMMANDS:')}
166
218
  ${chalk_1.default.cyan(`${PROGRAM_NAME} list`)}
167
219
  ${chalk_1.default.cyan(`${PROGRAM_NAME} list --type workflow`)}
168
220
 
221
+ ${chalk_1.default.bold.yellow('AUTH COMMANDS:')}
222
+ ${chalk_1.default.gray('# Login to a CX environment')}
223
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} login https://qa.storevista.acuitive.net`)}
224
+
225
+ ${chalk_1.default.gray('# Logout from current session')}
226
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} logout`)}
227
+
228
+ ${chalk_1.default.bold.yellow('PAT COMMANDS:')}
229
+ ${chalk_1.default.gray('# Check PAT token status and setup instructions')}
230
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat setup`)}
231
+
232
+ ${chalk_1.default.gray('# Create a new PAT token')}
233
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat create "my-token-name"`)}
234
+
235
+ ${chalk_1.default.gray('# List active PAT tokens')}
236
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat list`)}
237
+
238
+ ${chalk_1.default.gray('# Revoke a PAT token by ID')}
239
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat revoke <tokenId>`)}
240
+
241
+ ${chalk_1.default.bold.yellow('ORG COMMANDS:')}
242
+ ${chalk_1.default.gray('# List organizations on the server')}
243
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs list`)}
244
+
245
+ ${chalk_1.default.gray('# Interactively select an organization')}
246
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs select`)}
247
+
248
+ ${chalk_1.default.gray('# Set active organization by ID')}
249
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs use <orgId>`)}
250
+
251
+ ${chalk_1.default.gray('# Show current context (server, org, app)')}
252
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs use`)}
253
+
254
+ ${chalk_1.default.bold.yellow('APPMODULE COMMANDS:')}
255
+ ${chalk_1.default.gray('# Deploy a module YAML to the server (creates or updates)')}
256
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule deploy modules/my-module.yaml`)}
257
+
258
+ ${chalk_1.default.gray('# Deploy with explicit org ID')}
259
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule deploy modules/my-module.yaml --org 42`)}
260
+
261
+ ${chalk_1.default.gray('# Undeploy an app module by UUID')}
262
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule undeploy <appModuleId>`)}
263
+
264
+ ${chalk_1.default.bold.yellow('WORKFLOW COMMANDS:')}
265
+ ${chalk_1.default.gray('# Deploy a workflow YAML to the server (creates or updates)')}
266
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow deploy workflows/my-workflow.yaml`)}
267
+
268
+ ${chalk_1.default.gray('# Undeploy a workflow by UUID')}
269
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow undeploy <workflowId>`)}
270
+
271
+ ${chalk_1.default.gray('# Execute a workflow')}
272
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId|file.yaml>`)}
273
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId> --vars '{"city":"London"}'`)}
274
+
275
+ ${chalk_1.default.gray('# List execution logs for a workflow (sorted desc)')}
276
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <workflowId|file.yaml>`)}
277
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <workflowId> --from 2026-01-01 --to 2026-01-31`)}
278
+
279
+ ${chalk_1.default.gray('# Download a specific execution log')}
280
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId>`)} ${chalk_1.default.gray('# save txt log to temp dir')}
281
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --output log.txt`)} ${chalk_1.default.gray('# save to file')}
282
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --console`)} ${chalk_1.default.gray('# print to stdout')}
283
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --json`)} ${chalk_1.default.gray('# download JSON log (more detail)')}
284
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --json --console`)} ${chalk_1.default.gray('# JSON log to stdout')}
285
+
286
+ ${chalk_1.default.bold.yellow('PUBLISH COMMANDS:')}
287
+ ${chalk_1.default.gray('# Publish all modules and workflows from current project')}
288
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish`)}
289
+
290
+ ${chalk_1.default.gray('# Publish only a specific feature directory')}
291
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish --feature billing`)}
292
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish billing`)}
293
+
294
+ ${chalk_1.default.gray('# Publish with explicit org ID')}
295
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish --org 42`)}
296
+
297
+ ${chalk_1.default.bold.yellow('APP COMMANDS:')}
298
+ ${chalk_1.default.gray('# Install/refresh app from git repository into the CX server')}
299
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install`)}
300
+
301
+ ${chalk_1.default.gray('# Force reinstall even if same version')}
302
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install --force`)}
303
+
304
+ ${chalk_1.default.gray('# Install from a specific branch')}
305
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install --branch develop`)}
306
+
307
+ ${chalk_1.default.gray('# Install but skip modules that have local changes')}
308
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app install --skip-changed`)}
309
+
310
+ ${chalk_1.default.gray('# Upgrade app from git (alias for install)')}
311
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app upgrade`)}
312
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app upgrade --force`)}
313
+
314
+ ${chalk_1.default.gray('# Release server changes to git (creates a PR) — message is required')}
315
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Add new shipping module"`)}
316
+
317
+ ${chalk_1.default.gray('# Release specific workflows and/or modules by YAML file')}
318
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Fix order workflow" workflows/my-workflow.yaml`)}
319
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Update billing" workflows/a.yaml modules/b.yaml`)}
320
+
321
+ ${chalk_1.default.gray('# Force release all modules and workflows')}
322
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app release -m "Full republish" --force`)}
323
+
324
+ ${chalk_1.default.gray('# List installed app manifests on the server')}
325
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} app list`)}
326
+
327
+ ${chalk_1.default.bold.yellow('QUERY COMMANDS:')}
328
+ ${chalk_1.default.gray('# Run an inline GraphQL query')}
329
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} query '{ organizations(take: 5) { items { organizationId companyName } } }'`)}
330
+
331
+ ${chalk_1.default.gray('# Run a query from a .graphql file')}
332
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} query my-query.graphql`)}
333
+
334
+ ${chalk_1.default.gray('# Pass variables as JSON')}
335
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} query my-query.graphql --vars '{"id": 42}'`)}
336
+
337
+ ${chalk_1.default.bold.yellow('GRAPHQL SCHEMA EXPLORATION:')}
338
+ ${chalk_1.default.gray('# List all queries, mutations, and types')}
339
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql queries`)}
340
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql mutations`)}
341
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql types`)}
342
+
343
+ ${chalk_1.default.gray('# Filter by name')}
344
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql types --filter audit`)}
345
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql queries --filter order`)}
346
+
347
+ ${chalk_1.default.gray('# Inspect a specific type')}
348
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql type OrderGqlDto`)}
349
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} gql type AuditChangeEntry`)}
350
+
169
351
  ${chalk_1.default.bold.yellow('VALIDATION TYPES:')}
170
352
  ${chalk_1.default.bold('module')} - CargoXplorer UI module definitions (components, routes, entities)
171
353
  ${chalk_1.default.bold('workflow')} - CargoXplorer workflow definitions (activities, tasks, triggers)
@@ -182,6 +364,8 @@ ${chalk_1.default.bold.yellow('EXIT CODES:')}
182
364
  ${chalk_1.default.red('2')} - CLI error (invalid arguments, file not found, etc.)
183
365
 
184
366
  ${chalk_1.default.bold.yellow('ENVIRONMENT VARIABLES:')}
367
+ ${chalk_1.default.green('CXTMS_AUTH')} - PAT token for authentication (skips OAuth login)
368
+ ${chalk_1.default.green('CXTMS_SERVER')} - Server URL when using PAT auth (or set \`server\` in app.yaml)
185
369
  ${chalk_1.default.green('CX_SCHEMA_PATH')} - Default path to schemas directory
186
370
  ${chalk_1.default.green('NO_COLOR')} - Disable colored output
187
371
 
@@ -327,7 +511,8 @@ function generateAppYaml(name) {
327
511
  const dirName = path.basename(process.cwd());
328
512
  const appName = name || dirName;
329
513
  const scopedName = appName.startsWith('@') ? appName : `@cargox/${appName}`;
330
- return `name: "${scopedName}"
514
+ return `id: "${generateUUID()}"
515
+ name: "${scopedName}"
331
516
  description: ""
332
517
  author: "CargoX"
333
518
  version: "1.0.0"
@@ -367,39 +552,39 @@ npm install @cxtms/cx-schema
367
552
 
368
553
  \`\`\`bash
369
554
  # Validate all modules
370
- npx cx-cli modules/*.yaml
555
+ npx cxtms modules/*.yaml
371
556
 
372
557
  # Validate all workflows
373
- npx cx-cli workflows/*.yaml
558
+ npx cxtms workflows/*.yaml
374
559
 
375
560
  # Validate with detailed output
376
- npx cx-cli --verbose modules/my-module.yaml
561
+ npx cxtms --verbose modules/my-module.yaml
377
562
 
378
563
  # Generate validation report
379
- npx cx-cli report modules/*.yaml workflows/*.yaml --report report.html
564
+ npx cxtms report modules/*.yaml workflows/*.yaml --report report.html
380
565
  \`\`\`
381
566
 
382
567
  ### Create new files
383
568
 
384
569
  \`\`\`bash
385
570
  # Create a new module
386
- npx cx-cli create module my-module
571
+ npx cxtms create module my-module
387
572
 
388
573
  # Create a new workflow
389
- npx cx-cli create workflow my-workflow
574
+ npx cxtms create workflow my-workflow
390
575
  \`\`\`
391
576
 
392
577
  ### View schemas and examples
393
578
 
394
579
  \`\`\`bash
395
580
  # List available schemas
396
- npx cx-cli list
581
+ npx cxtms list
397
582
 
398
583
  # View schema for a component
399
- npx cx-cli schema form
584
+ npx cxtms schema form
400
585
 
401
586
  # View example YAML
402
- npx cx-cli example workflow
587
+ npx cxtms example workflow
403
588
  \`\`\`
404
589
 
405
590
  ## Documentation
@@ -420,13 +605,13 @@ When making changes to YAML files, always validate them:
420
605
 
421
606
  \`\`\`bash
422
607
  # Validate a specific module file
423
- npx cx-cli modules/<module-name>.yaml
608
+ npx cxtms modules/<module-name>.yaml
424
609
 
425
610
  # Validate a specific workflow file
426
- npx cx-cli workflows/<workflow-name>.yaml
611
+ npx cxtms workflows/<workflow-name>.yaml
427
612
 
428
613
  # Validate all files with a report
429
- npx cx-cli report modules/*.yaml workflows/*.yaml --report validation-report.md
614
+ npx cxtms report modules/*.yaml workflows/*.yaml --report validation-report.md
430
615
  \`\`\`
431
616
 
432
617
  ## Schema Reference
@@ -435,14 +620,14 @@ Before editing components or tasks, check the schema:
435
620
 
436
621
  \`\`\`bash
437
622
  # View schema for components
438
- npx cx-cli schema form
439
- npx cx-cli schema dataGrid
440
- npx cx-cli schema layout
623
+ npx cxtms schema form
624
+ npx cxtms schema dataGrid
625
+ npx cxtms schema layout
441
626
 
442
627
  # View schema for workflow tasks
443
- npx cx-cli schema foreach
444
- npx cx-cli schema graphql
445
- npx cx-cli schema switch
628
+ npx cxtms schema foreach
629
+ npx cxtms schema graphql
630
+ npx cxtms schema switch
446
631
  \`\`\`
447
632
 
448
633
  ## Creating New Files
@@ -451,16 +636,16 @@ Use templates to create properly structured files:
451
636
 
452
637
  \`\`\`bash
453
638
  # Create a new module
454
- npx cx-cli create module <name>
639
+ npx cxtms create module <name>
455
640
 
456
641
  # Create a new workflow
457
- npx cx-cli create workflow <name>
642
+ npx cxtms create workflow <name>
458
643
 
459
644
  # Create from a specific template variant
460
- npx cx-cli create workflow <name> --template basic
645
+ npx cxtms create workflow <name> --template basic
461
646
 
462
647
  # Create inside a feature folder (features/<name>/workflows/)
463
- npx cx-cli create workflow <name> --feature billing
648
+ npx cxtms create workflow <name> --feature billing
464
649
  \`\`\`
465
650
 
466
651
  ## Module Structure
@@ -705,6 +890,23 @@ function applyFieldsToForm(form, fields) {
705
890
  }
706
891
  }
707
892
  }
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
+ }
708
910
  function findDataGridComponents(obj) {
709
911
  const grids = [];
710
912
  if (!obj || typeof obj !== 'object')
@@ -832,9 +1034,16 @@ function applyCreateOptions(content, optionsArg) {
832
1034
  if (!doc)
833
1035
  throw new Error('Failed to parse template YAML for --options processing');
834
1036
  let applied = false;
1037
+ const isConfiguration = Array.isArray(doc.configurations);
835
1038
  if (doc.components && Array.isArray(doc.components)) {
836
1039
  for (const comp of doc.components) {
837
- // Apply to form components (configuration template)
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
838
1047
  const forms = findFormComponents(comp);
839
1048
  for (const form of forms) {
840
1049
  applyFieldsToForm(form, fields);
@@ -862,6 +1071,18 @@ function applyCreateOptions(content, optionsArg) {
862
1071
  applyFieldsToEntities(doc, fields, opts.entityName);
863
1072
  applied = true;
864
1073
  }
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
+ }
865
1086
  if (!applied) {
866
1087
  console.warn(chalk_1.default.yellow('Warning: --options provided but no form or dataGrid component found in template'));
867
1088
  return content;
@@ -1224,19 +1445,27 @@ function runUpdate() {
1224
1445
  const { execSync } = require('child_process');
1225
1446
  console.log(' Updating to latest version...\n');
1226
1447
  try {
1227
- const output = execSync('npm install @cxtms/cx-schema@latest', {
1448
+ execSync('npm install @cxtms/cx-schema@latest', {
1228
1449
  stdio: 'inherit',
1229
1450
  cwd: process.cwd()
1230
1451
  });
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
+ }
1231
1458
  console.log('');
1232
- console.log(chalk_1.default.green('✓ @cxtms/cx-schema updated successfully!'));
1233
- console.log('');
1459
+ console.log(chalk_1.default.green(`✓ @cxtms/cx-schema updated to v${installedVersion}`));
1234
1460
  }
1235
1461
  catch (error) {
1236
1462
  console.error(chalk_1.default.red('\nError: Failed to update @cxtms/cx-schema'));
1237
1463
  console.error(chalk_1.default.gray(error.message));
1238
1464
  process.exit(1);
1239
1465
  }
1466
+ // Reinstall skills and update CLAUDE.md (postinstall handles schemas)
1467
+ runInstallSkills();
1468
+ runSetupClaude();
1240
1469
  }
1241
1470
  // ============================================================================
1242
1471
  // Setup Claude Command
@@ -1260,23 +1489,23 @@ features/ # Feature-scoped modules and workflows
1260
1489
  workflows/
1261
1490
  \`\`\`
1262
1491
 
1263
- ### CLI — \`cx-cli\`
1492
+ ### CLI — \`cxtms\`
1264
1493
 
1265
1494
  **Always scaffold via CLI, never write YAML from scratch.**
1266
1495
 
1267
1496
  | Command | Description |
1268
1497
  |---------|-------------|
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 |
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 |
1280
1509
 
1281
1510
  **Module templates:** \`default\`, \`form\`, \`grid\`, \`select\`, \`configuration\`
1282
1511
  **Workflow templates:** \`basic\`, \`entity-trigger\`, \`document\`, \`scheduled\`, \`utility\`, \`webhook\`, \`public-api\`, \`mcp-tool\`, \`ftp-tracking\`, \`ftp-edi\`, \`api-tracking\`
@@ -1291,10 +1520,10 @@ features/ # Feature-scoped modules and workflows
1291
1520
 
1292
1521
  ### Workflow: Scaffold → Customize → Validate
1293
1522
 
1294
- 1. **Scaffold** — \`npx cx-cli create module|workflow <name> --template <t>\`
1523
+ 1. **Scaffold** — \`npx cxtms create module|workflow <name> --template <t>\`
1295
1524
  2. **Read** the generated file
1296
1525
  3. **Customize** for the use case
1297
- 4. **Validate** — \`npx cx-cli <file.yaml>\` — run after every change, fix all errors
1526
+ 4. **Validate** — \`npx cxtms <file.yaml>\` — run after every change, fix all errors
1298
1527
  ${CX_CLAUDE_MARKER}`;
1299
1528
  }
1300
1529
  function runSetupClaude() {
@@ -1329,151 +1558,1962 @@ function runSetupClaude() {
1329
1558
  console.log('');
1330
1559
  }
1331
1560
  // ============================================================================
1332
- // Extract Command
1561
+ // Auth (Login / Logout)
1333
1562
  // ============================================================================
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);
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'));
1340
1578
  }
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);
1579
+ catch {
1580
+ return null;
1345
1581
  }
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);
1582
+ }
1583
+ function writeSessionFile(data) {
1584
+ const dir = getSessionDir();
1585
+ if (!fs.existsSync(dir)) {
1586
+ fs.mkdirSync(dir, { recursive: true });
1353
1587
  }
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);
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);
1359
1594
  }
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;
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();
1363
1624
  });
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}`));
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
+ }
1371
1653
  }
1372
- }
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);
1663
+ }
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'));
1373
1745
  process.exit(2);
1374
1746
  }
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;
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.');
1386
1820
  }
1387
1821
  }
1388
- else {
1389
- componentComment = componentNode.commentBefore;
1822
+ // Try to parse GraphQL errors from 400 responses too
1823
+ let json;
1824
+ try {
1825
+ json = JSON.parse(res.body);
1390
1826
  }
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
- }
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(' — ');
1399
1842
  });
1843
+ throw new Error(`GraphQL error: ${messages.join('; ')}`);
1400
1844
  }
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];
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();
1407
1874
  });
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}`));
1417
- process.exit(2);
1418
- }
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}"`));
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'));
1423
1898
  process.exit(2);
1424
1899
  }
1425
- }
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',
1900
+ return {
1901
+ domain,
1902
+ client_id: '',
1903
+ access_token: patToken,
1904
+ refresh_token: '',
1905
+ expires_at: 0,
1441
1906
  };
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);
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);
1446
1938
  }
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;
1939
+ return orgId;
1457
1940
  }
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
- }
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
+ }
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);
1970
+ }
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 }
1470
2001
  }
1471
- tgtComponents.items.push(componentNode);
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`));
1472
2012
  }
1473
2013
  else {
1474
- tgtDoc.addIn(['components'], componentNode);
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`));
1475
2033
  }
1476
- // In move mode, remove component from source
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);
3515
+ }
3516
+ // In move mode, remove component from source
1477
3517
  if (!copy) {
1478
3518
  srcComponents.items.splice(compIndex, 1);
1479
3519
  }
@@ -1539,7 +3579,7 @@ function parseArgs(args) {
1539
3579
  reportFormat: 'json'
1540
3580
  };
1541
3581
  // Check for commands
1542
- const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude'];
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'];
1543
3583
  if (args.length > 0 && commands.includes(args[0])) {
1544
3584
  command = args[0];
1545
3585
  args = args.slice(1);
@@ -1615,6 +3655,50 @@ function parseArgs(args) {
1615
3655
  else if (arg === '--copy') {
1616
3656
  options.extractCopy = true;
1617
3657
  }
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
+ }
1618
3702
  else if (!arg.startsWith('-')) {
1619
3703
  files.push(arg);
1620
3704
  }
@@ -2021,13 +4105,13 @@ function getSuggestion(error) {
2021
4105
  return 'Check that the value type matches the expected type (string, number, boolean, etc.)';
2022
4106
  }
2023
4107
  if (error.message.includes('additionalProperties')) {
2024
- return 'Remove unrecognized properties. Use `cx-cli schema <type>` to see allowed properties.';
4108
+ return 'Remove unrecognized properties. Use `cxtms schema <type>` to see allowed properties.';
2025
4109
  }
2026
4110
  return 'Review the schema requirements for this property';
2027
4111
  case 'yaml_syntax_error':
2028
4112
  return 'Check YAML indentation and syntax. Use a YAML linter to identify issues.';
2029
4113
  case 'invalid_task_type':
2030
- return `Use 'cx-cli list --type workflow' to see available task types`;
4114
+ return `Use 'cxtms list --type workflow' to see available task types`;
2031
4115
  case 'invalid_activity':
2032
4116
  return 'Each activity must have a "name" and "steps" array';
2033
4117
  default:
@@ -2401,7 +4485,151 @@ async function main() {
2401
4485
  }
2402
4486
  // Handle version
2403
4487
  if (options.version) {
2404
- console.log(`cx-cli v${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);
2405
4633
  process.exit(0);
2406
4634
  }
2407
4635
  // Find schemas path