@cxtms/cx-schema 1.8.1 → 1.9.0

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