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