@cxtms/cx-schema 1.5.10 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -42,12 +42,43 @@ 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;
@@ -79,6 +110,13 @@ ${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 (push, delete)
118
+ ${chalk_1.default.green('workflow')} Manage workflows on a CX server (push, delete, execute, logs)
119
+ ${chalk_1.default.green('publish')} Publish all modules and workflows to a CX server
82
120
  ${chalk_1.default.green('schema')} Show JSON schema for a component or task
83
121
  ${chalk_1.default.green('example')} Show example YAML for a component or task
84
122
  ${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
@@ -101,6 +139,8 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
101
139
  ${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
102
140
  ${chalk_1.default.green('--to <file>')} Target file for extract command
103
141
  ${chalk_1.default.green('--copy')} Copy component instead of moving (source unchanged, target gets higher priority)
142
+ ${chalk_1.default.green('--org <id>')} Organization ID for server commands
143
+ ${chalk_1.default.green('--vars <json>')} JSON variables for workflow execute
104
144
 
105
145
  ${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
106
146
  ${chalk_1.default.gray('# Validate a module YAML file')}
@@ -166,6 +206,80 @@ ${chalk_1.default.bold.yellow('SCHEMA COMMANDS:')}
166
206
  ${chalk_1.default.cyan(`${PROGRAM_NAME} list`)}
167
207
  ${chalk_1.default.cyan(`${PROGRAM_NAME} list --type workflow`)}
168
208
 
209
+ ${chalk_1.default.bold.yellow('AUTH COMMANDS:')}
210
+ ${chalk_1.default.gray('# Login to a CX environment')}
211
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} login https://qa.storevista.acuitive.net`)}
212
+
213
+ ${chalk_1.default.gray('# Logout from a CX environment')}
214
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} logout https://qa.storevista.acuitive.net`)}
215
+
216
+ ${chalk_1.default.gray('# List stored sessions')}
217
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} logout`)}
218
+
219
+ ${chalk_1.default.bold.yellow('PAT COMMANDS:')}
220
+ ${chalk_1.default.gray('# Check PAT token status and setup instructions')}
221
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat setup`)}
222
+
223
+ ${chalk_1.default.gray('# Create a new PAT token')}
224
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat create "my-token-name"`)}
225
+
226
+ ${chalk_1.default.gray('# List active PAT tokens')}
227
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat list`)}
228
+
229
+ ${chalk_1.default.gray('# Revoke a PAT token by ID')}
230
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} pat revoke <tokenId>`)}
231
+
232
+ ${chalk_1.default.bold.yellow('ORG COMMANDS:')}
233
+ ${chalk_1.default.gray('# List organizations on the server')}
234
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs list`)}
235
+
236
+ ${chalk_1.default.gray('# Interactively select an organization')}
237
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs select`)}
238
+
239
+ ${chalk_1.default.gray('# Set active organization by ID')}
240
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs use <orgId>`)}
241
+
242
+ ${chalk_1.default.gray('# Show current context (server, org, app)')}
243
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} orgs use`)}
244
+
245
+ ${chalk_1.default.bold.yellow('APPMODULE COMMANDS:')}
246
+ ${chalk_1.default.gray('# Push a module YAML to the server (creates or updates)')}
247
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule push modules/my-module.yaml`)}
248
+
249
+ ${chalk_1.default.gray('# Push with explicit org ID')}
250
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule push modules/my-module.yaml --org 42`)}
251
+
252
+ ${chalk_1.default.gray('# Delete an app module by UUID')}
253
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} appmodule delete <appModuleId>`)}
254
+
255
+ ${chalk_1.default.bold.yellow('WORKFLOW COMMANDS:')}
256
+ ${chalk_1.default.gray('# Push a workflow YAML to the server (creates or updates)')}
257
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow push workflows/my-workflow.yaml`)}
258
+
259
+ ${chalk_1.default.gray('# Delete a workflow by UUID')}
260
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow delete <workflowId>`)}
261
+
262
+ ${chalk_1.default.gray('# Execute a workflow')}
263
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId|file.yaml>`)}
264
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId> --vars '{"city":"London"}'`)}
265
+
266
+ ${chalk_1.default.gray('# List recent executions for a workflow')}
267
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow executions <workflowId|file.yaml>`)}
268
+
269
+ ${chalk_1.default.gray('# Show execution details and download logs')}
270
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <executionId>`)}
271
+
272
+ ${chalk_1.default.bold.yellow('PUBLISH COMMANDS:')}
273
+ ${chalk_1.default.gray('# Publish all modules and workflows from current project')}
274
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish`)}
275
+
276
+ ${chalk_1.default.gray('# Publish only a specific feature directory')}
277
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish --feature billing`)}
278
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish billing`)}
279
+
280
+ ${chalk_1.default.gray('# Publish with explicit org ID')}
281
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} publish --org 42`)}
282
+
169
283
  ${chalk_1.default.bold.yellow('VALIDATION TYPES:')}
170
284
  ${chalk_1.default.bold('module')} - CargoXplorer UI module definitions (components, routes, entities)
171
285
  ${chalk_1.default.bold('workflow')} - CargoXplorer workflow definitions (activities, tasks, triggers)
@@ -182,6 +296,8 @@ ${chalk_1.default.bold.yellow('EXIT CODES:')}
182
296
  ${chalk_1.default.red('2')} - CLI error (invalid arguments, file not found, etc.)
183
297
 
184
298
  ${chalk_1.default.bold.yellow('ENVIRONMENT VARIABLES:')}
299
+ ${chalk_1.default.green('CXTMS_AUTH')} - PAT token for authentication (skips OAuth login)
300
+ ${chalk_1.default.green('CXTMS_SERVER')} - Server URL when using PAT auth (or set \`server\` in app.yaml)
185
301
  ${chalk_1.default.green('CX_SCHEMA_PATH')} - Default path to schemas directory
186
302
  ${chalk_1.default.green('NO_COLOR')} - Disable colored output
187
303
 
@@ -1338,6 +1454,1287 @@ function runSetupClaude() {
1338
1454
  console.log('');
1339
1455
  }
1340
1456
  // ============================================================================
1457
+ // Auth (Login / Logout)
1458
+ // ============================================================================
1459
+ const AUTH_CALLBACK_PORT = 9000;
1460
+ const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
1461
+ function getTokenDir() {
1462
+ return path.join(os.homedir(), '.cxtms');
1463
+ }
1464
+ function getTokenFilePath(domain) {
1465
+ const hostname = new URL(domain).hostname;
1466
+ return path.join(getTokenDir(), `${hostname}.json`);
1467
+ }
1468
+ function readTokenFile(domain) {
1469
+ const filePath = getTokenFilePath(domain);
1470
+ if (!fs.existsSync(filePath))
1471
+ return null;
1472
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1473
+ }
1474
+ function writeTokenFile(data) {
1475
+ const dir = getTokenDir();
1476
+ if (!fs.existsSync(dir)) {
1477
+ fs.mkdirSync(dir, { recursive: true });
1478
+ }
1479
+ fs.writeFileSync(getTokenFilePath(data.domain), JSON.stringify(data, null, 2), 'utf-8');
1480
+ }
1481
+ function deleteTokenFile(domain) {
1482
+ const filePath = getTokenFilePath(domain);
1483
+ if (fs.existsSync(filePath)) {
1484
+ fs.unlinkSync(filePath);
1485
+ }
1486
+ }
1487
+ function generateCodeVerifier() {
1488
+ return crypto.randomBytes(32).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
1489
+ }
1490
+ function generateCodeChallenge(verifier) {
1491
+ return crypto.createHash('sha256').update(verifier).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
1492
+ }
1493
+ function httpsPost(url, body, contentType) {
1494
+ return new Promise((resolve, reject) => {
1495
+ const parsed = new URL(url);
1496
+ const isHttps = parsed.protocol === 'https:';
1497
+ const lib = isHttps ? https : http;
1498
+ const req = lib.request({
1499
+ hostname: parsed.hostname,
1500
+ port: parsed.port || (isHttps ? 443 : 80),
1501
+ path: parsed.pathname + parsed.search,
1502
+ method: 'POST',
1503
+ headers: {
1504
+ 'Content-Type': contentType,
1505
+ 'Content-Length': Buffer.byteLength(body),
1506
+ },
1507
+ }, (res) => {
1508
+ let data = '';
1509
+ res.on('data', (chunk) => data += chunk);
1510
+ res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
1511
+ });
1512
+ req.on('error', reject);
1513
+ req.write(body);
1514
+ req.end();
1515
+ });
1516
+ }
1517
+ function openBrowser(url) {
1518
+ const { exec } = require('child_process');
1519
+ const cmd = process.platform === 'win32' ? `start "" "${url}"`
1520
+ : process.platform === 'darwin' ? `open "${url}"`
1521
+ : `xdg-open "${url}"`;
1522
+ exec(cmd);
1523
+ }
1524
+ function startCallbackServer() {
1525
+ return new Promise((resolve, reject) => {
1526
+ const server = http.createServer((req, res) => {
1527
+ const reqUrl = new URL(req.url || '/', `http://127.0.0.1:${AUTH_CALLBACK_PORT}`);
1528
+ if (reqUrl.pathname === '/callback') {
1529
+ const code = reqUrl.searchParams.get('code');
1530
+ const error = reqUrl.searchParams.get('error');
1531
+ if (error) {
1532
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1533
+ res.end('<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>');
1534
+ reject(new Error(`OAuth error: ${error} - ${reqUrl.searchParams.get('error_description') || ''}`));
1535
+ server.close();
1536
+ return;
1537
+ }
1538
+ if (code) {
1539
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1540
+ res.end('<html><body><h2>Login successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
1541
+ resolve({ code, close: () => server.close() });
1542
+ return;
1543
+ }
1544
+ }
1545
+ res.writeHead(404);
1546
+ res.end();
1547
+ });
1548
+ server.on('error', (err) => {
1549
+ if (err.code === 'EADDRINUSE') {
1550
+ reject(new Error(`Port ${AUTH_CALLBACK_PORT} is already in use. Close the process using it and try again.`));
1551
+ }
1552
+ else {
1553
+ reject(err);
1554
+ }
1555
+ });
1556
+ server.listen(AUTH_CALLBACK_PORT, '127.0.0.1');
1557
+ });
1558
+ }
1559
+ async function registerOAuthClient(domain) {
1560
+ const res = await httpsPost(`${domain}/connect/register`, JSON.stringify({
1561
+ client_name: `cx-cli-${crypto.randomBytes(4).toString('hex')}`,
1562
+ redirect_uris: [`http://localhost:${AUTH_CALLBACK_PORT}/callback`],
1563
+ grant_types: ['authorization_code', 'refresh_token'],
1564
+ response_types: ['code'],
1565
+ token_endpoint_auth_method: 'none',
1566
+ }), 'application/json');
1567
+ if (res.statusCode !== 200 && res.statusCode !== 201) {
1568
+ throw new Error(`Client registration failed (${res.statusCode}): ${res.body}`);
1569
+ }
1570
+ const data = JSON.parse(res.body);
1571
+ if (!data.client_id) {
1572
+ throw new Error('Client registration response missing client_id');
1573
+ }
1574
+ return data.client_id;
1575
+ }
1576
+ async function exchangeCodeForTokens(domain, clientId, code, codeVerifier) {
1577
+ const body = new URLSearchParams({
1578
+ grant_type: 'authorization_code',
1579
+ client_id: clientId,
1580
+ code,
1581
+ redirect_uri: `http://localhost:${AUTH_CALLBACK_PORT}/callback`,
1582
+ code_verifier: codeVerifier,
1583
+ }).toString();
1584
+ const res = await httpsPost(`${domain}/connect/token`, body, 'application/x-www-form-urlencoded');
1585
+ if (res.statusCode !== 200) {
1586
+ throw new Error(`Token exchange failed (${res.statusCode}): ${res.body}`);
1587
+ }
1588
+ const data = JSON.parse(res.body);
1589
+ return {
1590
+ domain,
1591
+ client_id: clientId,
1592
+ access_token: data.access_token,
1593
+ refresh_token: data.refresh_token,
1594
+ expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
1595
+ };
1596
+ }
1597
+ async function revokeToken(domain, clientId, token) {
1598
+ try {
1599
+ await httpsPost(`${domain}/connect/revoke`, new URLSearchParams({ client_id: clientId, token }).toString(), 'application/x-www-form-urlencoded');
1600
+ }
1601
+ catch {
1602
+ // Revocation failures are non-fatal
1603
+ }
1604
+ }
1605
+ async function refreshTokens(stored) {
1606
+ const body = new URLSearchParams({
1607
+ grant_type: 'refresh_token',
1608
+ client_id: stored.client_id,
1609
+ refresh_token: stored.refresh_token,
1610
+ }).toString();
1611
+ const res = await httpsPost(`${stored.domain}/connect/token`, body, 'application/x-www-form-urlencoded');
1612
+ if (res.statusCode !== 200) {
1613
+ throw new Error(`Token refresh failed (${res.statusCode}): ${res.body}`);
1614
+ }
1615
+ const data = JSON.parse(res.body);
1616
+ const updated = {
1617
+ ...stored,
1618
+ access_token: data.access_token,
1619
+ refresh_token: data.refresh_token || stored.refresh_token,
1620
+ expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
1621
+ };
1622
+ writeTokenFile(updated);
1623
+ return updated;
1624
+ }
1625
+ async function runLogin(domain) {
1626
+ // Normalize URL
1627
+ if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
1628
+ domain = `https://${domain}`;
1629
+ }
1630
+ domain = domain.replace(/\/+$/, '');
1631
+ try {
1632
+ new URL(domain);
1633
+ }
1634
+ catch {
1635
+ console.error(chalk_1.default.red('Error: Invalid URL'));
1636
+ process.exit(2);
1637
+ }
1638
+ console.log(chalk_1.default.bold.cyan('\n CX CLI Login\n'));
1639
+ // Step 1: Register client
1640
+ console.log(chalk_1.default.gray(' Registering OAuth client...'));
1641
+ const clientId = await registerOAuthClient(domain);
1642
+ console.log(chalk_1.default.green(' ✓ Client registered'));
1643
+ // Step 2: PKCE
1644
+ const codeVerifier = generateCodeVerifier();
1645
+ const codeChallenge = generateCodeChallenge(codeVerifier);
1646
+ // Step 3: Start callback server
1647
+ const callbackPromise = startCallbackServer();
1648
+ // Step 4: Open browser
1649
+ const authUrl = `${domain}/connect/authorize?` + new URLSearchParams({
1650
+ client_id: clientId,
1651
+ redirect_uri: `http://localhost:${AUTH_CALLBACK_PORT}/callback`,
1652
+ response_type: 'code',
1653
+ scope: 'openid offline_access TMS.ApiAPI',
1654
+ code_challenge: codeChallenge,
1655
+ code_challenge_method: 'S256',
1656
+ }).toString();
1657
+ console.log(chalk_1.default.gray(' Opening browser for login...'));
1658
+ openBrowser(authUrl);
1659
+ console.log(chalk_1.default.gray(` Waiting for login (timeout: 2 min)...`));
1660
+ // Step 5: Wait for callback with timeout
1661
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Login timed out after 2 minutes. Please try again.')), AUTH_TIMEOUT_MS));
1662
+ const { code, close } = await Promise.race([callbackPromise, timeoutPromise]);
1663
+ // Step 6: Exchange code for tokens
1664
+ console.log(chalk_1.default.gray(' Exchanging authorization code...'));
1665
+ const tokens = await exchangeCodeForTokens(domain, clientId, code, codeVerifier);
1666
+ // Step 7: Store tokens
1667
+ writeTokenFile(tokens);
1668
+ close();
1669
+ console.log(chalk_1.default.green(` ✓ Logged in to ${new URL(domain).hostname}`));
1670
+ console.log(chalk_1.default.gray(` Token stored at: ${getTokenFilePath(domain)}\n`));
1671
+ }
1672
+ async function runLogout(domain) {
1673
+ if (!domain) {
1674
+ // List stored sessions
1675
+ const dir = getTokenDir();
1676
+ if (!fs.existsSync(dir)) {
1677
+ console.log(chalk_1.default.gray('\n No stored sessions.\n'));
1678
+ return;
1679
+ }
1680
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
1681
+ if (files.length === 0) {
1682
+ console.log(chalk_1.default.gray('\n No stored sessions.\n'));
1683
+ return;
1684
+ }
1685
+ console.log(chalk_1.default.bold.cyan('\n Stored sessions:\n'));
1686
+ for (const f of files) {
1687
+ const hostname = f.replace('.json', '');
1688
+ console.log(chalk_1.default.white(` ${hostname}`));
1689
+ }
1690
+ console.log(chalk_1.default.gray(`\n Usage: ${PROGRAM_NAME} logout <url>\n`));
1691
+ return;
1692
+ }
1693
+ // Normalize URL
1694
+ if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
1695
+ domain = `https://${domain}`;
1696
+ }
1697
+ domain = domain.replace(/\/+$/, '');
1698
+ const stored = readTokenFile(domain);
1699
+ if (!stored) {
1700
+ console.log(chalk_1.default.gray(`\n No session found for ${new URL(domain).hostname}\n`));
1701
+ return;
1702
+ }
1703
+ console.log(chalk_1.default.bold.cyan('\n CX CLI Logout\n'));
1704
+ // Revoke tokens (non-fatal)
1705
+ console.log(chalk_1.default.gray(' Revoking tokens...'));
1706
+ await revokeToken(domain, stored.client_id, stored.access_token);
1707
+ await revokeToken(domain, stored.client_id, stored.refresh_token);
1708
+ // Delete local file
1709
+ deleteTokenFile(domain);
1710
+ console.log(chalk_1.default.green(` ✓ Logged out from ${new URL(domain).hostname}\n`));
1711
+ }
1712
+ // ============================================================================
1713
+ // AppModule Commands
1714
+ // ============================================================================
1715
+ async function graphqlRequest(domain, token, query, variables) {
1716
+ const body = JSON.stringify({ query, variables });
1717
+ let res = await graphqlPostWithAuth(domain, token, body);
1718
+ if (res.statusCode === 401) {
1719
+ // PAT tokens have no refresh — fail immediately
1720
+ if (process.env.CXTMS_AUTH)
1721
+ throw new Error('PAT token unauthorized (401). Check your CXTMS_AUTH token.');
1722
+ // Try refresh for OAuth sessions
1723
+ const stored = readTokenFile(domain);
1724
+ if (!stored)
1725
+ throw new Error('Session expired. Run `cx-cli login <url>` again.');
1726
+ try {
1727
+ const refreshed = await refreshTokens(stored);
1728
+ res = await graphqlPostWithAuth(domain, refreshed.access_token, body);
1729
+ }
1730
+ catch {
1731
+ throw new Error('Session expired. Run `cx-cli login <url>` again.');
1732
+ }
1733
+ }
1734
+ // Try to parse GraphQL errors from 400 responses too
1735
+ let json;
1736
+ try {
1737
+ json = JSON.parse(res.body);
1738
+ }
1739
+ catch {
1740
+ if (res.statusCode !== 200) {
1741
+ throw new Error(`GraphQL request failed (${res.statusCode}): ${res.body}`);
1742
+ }
1743
+ throw new Error('Invalid JSON response from GraphQL endpoint');
1744
+ }
1745
+ if (json.errors && json.errors.length > 0) {
1746
+ const messages = json.errors.map((e) => {
1747
+ const ext = e.extensions?.message;
1748
+ return ext && ext !== e.message ? `${e.message} — ${ext}` : e.message;
1749
+ });
1750
+ throw new Error(`GraphQL error: ${messages.join('; ')}`);
1751
+ }
1752
+ if (res.statusCode !== 200) {
1753
+ throw new Error(`GraphQL request failed (${res.statusCode}): ${res.body}`);
1754
+ }
1755
+ return json.data;
1756
+ }
1757
+ function graphqlPostWithAuth(domain, token, body) {
1758
+ return new Promise((resolve, reject) => {
1759
+ const url = `${domain}/api/graphql`;
1760
+ const parsed = new URL(url);
1761
+ const isHttps = parsed.protocol === 'https:';
1762
+ const lib = isHttps ? https : http;
1763
+ const req = lib.request({
1764
+ hostname: parsed.hostname,
1765
+ port: parsed.port || (isHttps ? 443 : 80),
1766
+ path: parsed.pathname + parsed.search,
1767
+ method: 'POST',
1768
+ headers: {
1769
+ 'Content-Type': 'application/json',
1770
+ 'Content-Length': Buffer.byteLength(body),
1771
+ 'Authorization': `Bearer ${token}`,
1772
+ },
1773
+ }, (res) => {
1774
+ let data = '';
1775
+ res.on('data', (chunk) => data += chunk);
1776
+ res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: data }));
1777
+ });
1778
+ req.on('error', reject);
1779
+ req.write(body);
1780
+ req.end();
1781
+ });
1782
+ }
1783
+ function resolveDomainFromAppYaml() {
1784
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
1785
+ if (!fs.existsSync(appYamlPath))
1786
+ return null;
1787
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
1788
+ const serverDomain = appYaml?.server || appYaml?.domain;
1789
+ if (!serverDomain)
1790
+ return null;
1791
+ let domain = serverDomain;
1792
+ if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
1793
+ domain = `https://${domain}`;
1794
+ }
1795
+ return domain.replace(/\/+$/, '');
1796
+ }
1797
+ function resolveSession() {
1798
+ // 0. Check for PAT token in env (CXTMS_AUTH) — skips OAuth entirely
1799
+ const patToken = process.env.CXTMS_AUTH;
1800
+ if (patToken) {
1801
+ const domain = process.env.CXTMS_SERVER ? process.env.CXTMS_SERVER.replace(/\/+$/, '') : resolveDomainFromAppYaml();
1802
+ if (!domain) {
1803
+ console.error(chalk_1.default.red('CXTMS_AUTH is set but no server domain found.'));
1804
+ console.error(chalk_1.default.gray('Add `server` to app.yaml or set CXTMS_SERVER in .env'));
1805
+ process.exit(2);
1806
+ }
1807
+ return {
1808
+ domain,
1809
+ client_id: '',
1810
+ access_token: patToken,
1811
+ refresh_token: '',
1812
+ expires_at: 0,
1813
+ };
1814
+ }
1815
+ // 1. Check app.yaml in CWD for server field
1816
+ const appDomain = resolveDomainFromAppYaml();
1817
+ if (appDomain) {
1818
+ const stored = readTokenFile(appDomain);
1819
+ if (stored)
1820
+ return stored;
1821
+ console.error(chalk_1.default.red(`Not logged in to ${appDomain}. Run \`cx-cli login ${appDomain}\` first.`));
1822
+ process.exit(2);
1823
+ }
1824
+ // 2. Check for single session
1825
+ const dir = getTokenDir();
1826
+ if (!fs.existsSync(dir)) {
1827
+ console.error(chalk_1.default.red('Not logged in. Run `cx-cli login <url>` first.'));
1828
+ process.exit(2);
1829
+ }
1830
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
1831
+ if (files.length === 0) {
1832
+ console.error(chalk_1.default.red('Not logged in. Run `cx-cli login <url>` first.'));
1833
+ process.exit(2);
1834
+ }
1835
+ if (files.length === 1) {
1836
+ return JSON.parse(fs.readFileSync(path.join(dir, files[0]), 'utf-8'));
1837
+ }
1838
+ // 3. Multiple sessions — error
1839
+ console.error(chalk_1.default.red('Multiple sessions found:'));
1840
+ for (const f of files) {
1841
+ console.error(chalk_1.default.white(` ${f.replace('.json', '')}`));
1842
+ }
1843
+ console.error(chalk_1.default.gray('Add `server` field to app.yaml or use a single login session.'));
1844
+ process.exit(2);
1845
+ }
1846
+ async function resolveOrgId(domain, token, override) {
1847
+ // 1. Explicit override
1848
+ if (override !== undefined)
1849
+ return override;
1850
+ // 2. Cached in token file
1851
+ const stored = readTokenFile(domain);
1852
+ if (stored?.organization_id)
1853
+ return stored.organization_id;
1854
+ // 3. Query server
1855
+ const data = await graphqlRequest(domain, token, `
1856
+ query { organizations(take: 100) { items { organizationId companyName } } }
1857
+ `, {});
1858
+ const orgs = data?.organizations?.items;
1859
+ if (!orgs || orgs.length === 0) {
1860
+ throw new Error('No organizations found for this account.');
1861
+ }
1862
+ if (orgs.length === 1) {
1863
+ const orgId = orgs[0].organizationId;
1864
+ // Cache it
1865
+ if (stored) {
1866
+ stored.organization_id = orgId;
1867
+ writeTokenFile(stored);
1868
+ }
1869
+ return orgId;
1870
+ }
1871
+ // Multiple orgs — list and exit
1872
+ console.error(chalk_1.default.yellow('\n Multiple organizations found:\n'));
1873
+ for (const org of orgs) {
1874
+ console.error(chalk_1.default.white(` ${org.organizationId} ${org.companyName}`));
1875
+ }
1876
+ console.error(chalk_1.default.gray(`\n Run \`cx-cli orgs select\` to choose, or pass --org <id>.\n`));
1877
+ process.exit(2);
1878
+ }
1879
+ async function runAppModulePush(file, orgOverride) {
1880
+ if (!file) {
1881
+ console.error(chalk_1.default.red('Error: File path required'));
1882
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule push <file.yaml> [--org <id>]`));
1883
+ process.exit(2);
1884
+ }
1885
+ if (!fs.existsSync(file)) {
1886
+ console.error(chalk_1.default.red(`Error: File not found: ${file}`));
1887
+ process.exit(2);
1888
+ }
1889
+ const session = resolveSession();
1890
+ const domain = session.domain;
1891
+ const token = session.access_token;
1892
+ const orgId = await resolveOrgId(domain, token, orgOverride);
1893
+ // Read and parse YAML
1894
+ const yamlContent = fs.readFileSync(file, 'utf-8');
1895
+ const parsed = yaml_1.default.parse(yamlContent);
1896
+ const appModuleId = parsed?.module?.appModuleId;
1897
+ if (!appModuleId) {
1898
+ console.error(chalk_1.default.red('Error: Module YAML is missing module.appModuleId'));
1899
+ process.exit(2);
1900
+ }
1901
+ // Read app.yaml for appManifestId, fall back to cached session
1902
+ let appManifestId;
1903
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
1904
+ if (fs.existsSync(appYamlPath)) {
1905
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
1906
+ appManifestId = appYaml?.id;
1907
+ }
1908
+ if (!appManifestId && session.app_manifest_id) {
1909
+ appManifestId = session.app_manifest_id;
1910
+ }
1911
+ console.log(chalk_1.default.bold.cyan('\n AppModule Push\n'));
1912
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
1913
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
1914
+ console.log(chalk_1.default.gray(` Module: ${appModuleId}`));
1915
+ console.log('');
1916
+ // Check if module exists
1917
+ const checkData = await graphqlRequest(domain, token, `
1918
+ query ($organizationId: Int!, $appModuleId: UUID!) {
1919
+ appModule(organizationId: $organizationId, appModuleId: $appModuleId) {
1920
+ appModuleId
1921
+ }
1922
+ }
1923
+ `, { organizationId: orgId, appModuleId });
1924
+ if (checkData?.appModule) {
1925
+ // Update
1926
+ console.log(chalk_1.default.gray(' Updating existing module...'));
1927
+ const result = await graphqlRequest(domain, token, `
1928
+ mutation ($input: UpdateAppModuleInput!) {
1929
+ updateAppModule(input: $input) {
1930
+ appModule { appModuleId name }
1931
+ }
1932
+ }
1933
+ `, {
1934
+ input: {
1935
+ organizationId: orgId,
1936
+ appModuleId,
1937
+ values: { appModuleYamlDocument: yamlContent },
1938
+ },
1939
+ });
1940
+ const mod = result?.updateAppModule?.appModule;
1941
+ console.log(chalk_1.default.green(` ✓ Updated: ${mod?.name || appModuleId}\n`));
1942
+ }
1943
+ else {
1944
+ // Create
1945
+ console.log(chalk_1.default.gray(' Creating new module...'));
1946
+ const values = { appModuleYamlDocument: yamlContent };
1947
+ if (appManifestId)
1948
+ values.appManifestId = appManifestId;
1949
+ const result = await graphqlRequest(domain, token, `
1950
+ mutation ($input: CreateAppModuleInput!) {
1951
+ createAppModule(input: $input) {
1952
+ appModule { appModuleId name }
1953
+ }
1954
+ }
1955
+ `, {
1956
+ input: {
1957
+ organizationId: orgId,
1958
+ values,
1959
+ },
1960
+ });
1961
+ const mod = result?.createAppModule?.appModule;
1962
+ console.log(chalk_1.default.green(` ✓ Created: ${mod?.name || appModuleId}\n`));
1963
+ }
1964
+ }
1965
+ async function runAppModuleDelete(uuid, orgOverride) {
1966
+ if (!uuid) {
1967
+ console.error(chalk_1.default.red('Error: AppModule ID required'));
1968
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule delete <appModuleId> [--org <id>]`));
1969
+ process.exit(2);
1970
+ }
1971
+ const session = resolveSession();
1972
+ const domain = session.domain;
1973
+ const token = session.access_token;
1974
+ const orgId = await resolveOrgId(domain, token, orgOverride);
1975
+ console.log(chalk_1.default.bold.cyan('\n AppModule Delete\n'));
1976
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
1977
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
1978
+ console.log(chalk_1.default.gray(` Module: ${uuid}`));
1979
+ console.log('');
1980
+ await graphqlRequest(domain, token, `
1981
+ mutation ($input: DeleteAppModuleInput!) {
1982
+ deleteAppModule(input: $input) {
1983
+ deleteResult { __typename }
1984
+ }
1985
+ }
1986
+ `, {
1987
+ input: {
1988
+ organizationId: orgId,
1989
+ appModuleId: uuid,
1990
+ },
1991
+ });
1992
+ console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
1993
+ }
1994
+ async function runOrgsList() {
1995
+ const session = resolveSession();
1996
+ const domain = session.domain;
1997
+ const token = session.access_token;
1998
+ const data = await graphqlRequest(domain, token, `
1999
+ query { organizations(take: 100) { items { organizationId companyName } } }
2000
+ `, {});
2001
+ const orgs = data?.organizations?.items;
2002
+ if (!orgs || orgs.length === 0) {
2003
+ console.log(chalk_1.default.gray('\n No organizations found.\n'));
2004
+ return;
2005
+ }
2006
+ console.log(chalk_1.default.bold.cyan('\n Organizations\n'));
2007
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}\n`));
2008
+ for (const org of orgs) {
2009
+ const current = session.organization_id === org.organizationId;
2010
+ const marker = current ? chalk_1.default.green(' ← current') : '';
2011
+ console.log(chalk_1.default.white(` ${org.organizationId} ${org.companyName}${marker}`));
2012
+ }
2013
+ console.log('');
2014
+ }
2015
+ async function runOrgsUse(orgIdStr) {
2016
+ if (!orgIdStr) {
2017
+ // Show current context
2018
+ const session = resolveSession();
2019
+ const domain = session.domain;
2020
+ console.log(chalk_1.default.bold.cyan('\n Current Context\n'));
2021
+ console.log(chalk_1.default.white(` Server: ${new URL(domain).hostname}`));
2022
+ if (session.organization_id) {
2023
+ console.log(chalk_1.default.white(` Org: ${session.organization_id}`));
2024
+ }
2025
+ else {
2026
+ console.log(chalk_1.default.gray(` Org: (not set)`));
2027
+ }
2028
+ if (session.app_manifest_id) {
2029
+ console.log(chalk_1.default.white(` App: ${session.app_manifest_id}`));
2030
+ }
2031
+ else {
2032
+ // Try reading from app.yaml
2033
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
2034
+ if (fs.existsSync(appYamlPath)) {
2035
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
2036
+ if (appYaml?.id) {
2037
+ console.log(chalk_1.default.white(` App: ${appYaml.id} ${chalk_1.default.gray('(from app.yaml)')}`));
2038
+ }
2039
+ else {
2040
+ console.log(chalk_1.default.gray(` App: (not set)`));
2041
+ }
2042
+ }
2043
+ else {
2044
+ console.log(chalk_1.default.gray(` App: (not set)`));
2045
+ }
2046
+ }
2047
+ console.log('');
2048
+ return;
2049
+ }
2050
+ const orgId = parseInt(orgIdStr, 10);
2051
+ if (isNaN(orgId)) {
2052
+ console.error(chalk_1.default.red(`Invalid organization ID: ${orgIdStr}. Must be a number.`));
2053
+ process.exit(2);
2054
+ }
2055
+ const session = resolveSession();
2056
+ const domain = session.domain;
2057
+ const token = session.access_token;
2058
+ // Validate the org exists
2059
+ const data = await graphqlRequest(domain, token, `
2060
+ query { organizations(take: 100) { items { organizationId companyName } } }
2061
+ `, {});
2062
+ const orgs = data?.organizations?.items;
2063
+ const match = orgs?.find((o) => o.organizationId === orgId);
2064
+ if (!match) {
2065
+ console.error(chalk_1.default.red(`Organization ${orgId} not found.`));
2066
+ if (orgs?.length) {
2067
+ console.error(chalk_1.default.gray('\n Available organizations:'));
2068
+ for (const org of orgs) {
2069
+ console.error(chalk_1.default.white(` ${org.organizationId} ${org.companyName}`));
2070
+ }
2071
+ }
2072
+ console.error('');
2073
+ process.exit(2);
2074
+ }
2075
+ // Save to token file
2076
+ session.organization_id = orgId;
2077
+ writeTokenFile(session);
2078
+ console.log(chalk_1.default.green(`\n ✓ Context set to: ${match.companyName} (${orgId})\n`));
2079
+ }
2080
+ async function runOrgsSelect() {
2081
+ const session = resolveSession();
2082
+ const domain = session.domain;
2083
+ const token = session.access_token;
2084
+ const data = await graphqlRequest(domain, token, `
2085
+ query { organizations(take: 100) { items { organizationId companyName } } }
2086
+ `, {});
2087
+ const orgs = data?.organizations?.items;
2088
+ if (!orgs || orgs.length === 0) {
2089
+ console.log(chalk_1.default.gray('\n No organizations found.\n'));
2090
+ return;
2091
+ }
2092
+ console.log(chalk_1.default.bold.cyan('\n Select Organization\n'));
2093
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}\n`));
2094
+ for (let i = 0; i < orgs.length; i++) {
2095
+ const org = orgs[i];
2096
+ const current = session.organization_id === org.organizationId;
2097
+ const marker = current ? chalk_1.default.green(' ← current') : '';
2098
+ console.log(chalk_1.default.white(` ${i + 1}) ${org.organizationId} ${org.companyName}${marker}`));
2099
+ }
2100
+ console.log('');
2101
+ const readline = require('readline');
2102
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2103
+ const answer = await new Promise((resolve) => {
2104
+ rl.question(chalk_1.default.yellow(' Enter number: '), (ans) => {
2105
+ rl.close();
2106
+ resolve(ans.trim());
2107
+ });
2108
+ });
2109
+ const idx = parseInt(answer, 10) - 1;
2110
+ if (isNaN(idx) || idx < 0 || idx >= orgs.length) {
2111
+ console.error(chalk_1.default.red('\n Invalid selection.\n'));
2112
+ process.exit(2);
2113
+ }
2114
+ const selected = orgs[idx];
2115
+ session.organization_id = selected.organizationId;
2116
+ writeTokenFile(session);
2117
+ console.log(chalk_1.default.green(`\n ✓ Context set to: ${selected.companyName} (${selected.organizationId})\n`));
2118
+ }
2119
+ // ============================================================================
2120
+ // Workflow Commands
2121
+ // ============================================================================
2122
+ async function runWorkflowPush(file, orgOverride) {
2123
+ if (!file) {
2124
+ console.error(chalk_1.default.red('Error: File path required'));
2125
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow push <file.yaml> [--org <id>]`));
2126
+ process.exit(2);
2127
+ }
2128
+ if (!fs.existsSync(file)) {
2129
+ console.error(chalk_1.default.red(`Error: File not found: ${file}`));
2130
+ process.exit(2);
2131
+ }
2132
+ const session = resolveSession();
2133
+ const domain = session.domain;
2134
+ const token = session.access_token;
2135
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2136
+ const yamlContent = fs.readFileSync(file, 'utf-8');
2137
+ const parsed = yaml_1.default.parse(yamlContent);
2138
+ const workflowId = parsed?.workflow?.workflowId;
2139
+ if (!workflowId) {
2140
+ console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2141
+ process.exit(2);
2142
+ }
2143
+ const workflowName = parsed?.workflow?.name || workflowId;
2144
+ console.log(chalk_1.default.bold.cyan('\n Workflow Push\n'));
2145
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2146
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2147
+ console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
2148
+ console.log('');
2149
+ // Check if workflow exists
2150
+ const checkData = await graphqlRequest(domain, token, `
2151
+ query ($organizationId: Int!, $workflowId: UUID!) {
2152
+ workflow(organizationId: $organizationId, workflowId: $workflowId) {
2153
+ workflowId
2154
+ }
2155
+ }
2156
+ `, { organizationId: orgId, workflowId });
2157
+ if (checkData?.workflow) {
2158
+ console.log(chalk_1.default.gray(' Updating existing workflow...'));
2159
+ const result = await graphqlRequest(domain, token, `
2160
+ mutation ($input: UpdateWorkflowInput!) {
2161
+ updateWorkflow(input: $input) {
2162
+ workflow { workflowId }
2163
+ }
2164
+ }
2165
+ `, {
2166
+ input: {
2167
+ organizationId: orgId,
2168
+ workflowId,
2169
+ workflowYamlDocument: yamlContent,
2170
+ },
2171
+ });
2172
+ console.log(chalk_1.default.green(` ✓ Updated: ${workflowName}\n`));
2173
+ }
2174
+ else {
2175
+ console.log(chalk_1.default.gray(' Creating new workflow...'));
2176
+ const result = await graphqlRequest(domain, token, `
2177
+ mutation ($input: CreateWorkflowInput!) {
2178
+ createWorkflow(input: $input) {
2179
+ workflow { workflowId }
2180
+ }
2181
+ }
2182
+ `, {
2183
+ input: {
2184
+ organizationId: orgId,
2185
+ workflowYamlDocument: yamlContent,
2186
+ },
2187
+ });
2188
+ console.log(chalk_1.default.green(` ✓ Created: ${workflowName}\n`));
2189
+ }
2190
+ }
2191
+ async function runWorkflowDelete(uuid, orgOverride) {
2192
+ if (!uuid) {
2193
+ console.error(chalk_1.default.red('Error: Workflow ID required'));
2194
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow delete <workflowId> [--org <id>]`));
2195
+ process.exit(2);
2196
+ }
2197
+ const session = resolveSession();
2198
+ const domain = session.domain;
2199
+ const token = session.access_token;
2200
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2201
+ console.log(chalk_1.default.bold.cyan('\n Workflow Delete\n'));
2202
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2203
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2204
+ console.log(chalk_1.default.gray(` Workflow: ${uuid}`));
2205
+ console.log('');
2206
+ await graphqlRequest(domain, token, `
2207
+ mutation ($input: DeleteWorkflowInput!) {
2208
+ deleteWorkflow(input: $input) {
2209
+ deleteResult { __typename }
2210
+ }
2211
+ }
2212
+ `, {
2213
+ input: {
2214
+ organizationId: orgId,
2215
+ workflowId: uuid,
2216
+ },
2217
+ });
2218
+ console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
2219
+ }
2220
+ async function runWorkflowExecute(workflowIdOrFile, orgOverride, variables) {
2221
+ if (!workflowIdOrFile) {
2222
+ console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
2223
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow execute <workflowId|file.yaml> [--org <id>] [--vars '{"key":"value"}']`));
2224
+ process.exit(2);
2225
+ }
2226
+ const session = resolveSession();
2227
+ const { domain, access_token: token } = session;
2228
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2229
+ // Resolve workflowId
2230
+ let workflowId = workflowIdOrFile;
2231
+ let workflowName = workflowIdOrFile;
2232
+ if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
2233
+ if (!fs.existsSync(workflowIdOrFile)) {
2234
+ console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
2235
+ process.exit(2);
2236
+ }
2237
+ const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
2238
+ workflowId = parsed?.workflow?.workflowId;
2239
+ workflowName = parsed?.workflow?.name || path.basename(workflowIdOrFile);
2240
+ if (!workflowId) {
2241
+ console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2242
+ process.exit(2);
2243
+ }
2244
+ }
2245
+ // Parse variables if provided
2246
+ let vars;
2247
+ if (variables) {
2248
+ try {
2249
+ vars = JSON.parse(variables);
2250
+ }
2251
+ catch {
2252
+ console.error(chalk_1.default.red('Error: --vars must be valid JSON'));
2253
+ process.exit(2);
2254
+ }
2255
+ }
2256
+ console.log(chalk_1.default.bold.cyan('\n Workflow Execute\n'));
2257
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2258
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2259
+ console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
2260
+ if (vars)
2261
+ console.log(chalk_1.default.gray(` Variables: ${JSON.stringify(vars)}`));
2262
+ console.log('');
2263
+ const input = { organizationId: orgId, workflowId };
2264
+ if (vars)
2265
+ input.variables = vars;
2266
+ const data = await graphqlRequest(domain, token, `
2267
+ mutation ($input: ExecuteWorkflowInput!) {
2268
+ executeWorkflow(input: $input) {
2269
+ workflowExecutionResult {
2270
+ executionId workflowId isAsync workflowType outputs
2271
+ }
2272
+ }
2273
+ }
2274
+ `, { input });
2275
+ const result = data?.executeWorkflow?.workflowExecutionResult;
2276
+ if (!result) {
2277
+ console.error(chalk_1.default.red(' No execution result returned.\n'));
2278
+ process.exit(2);
2279
+ }
2280
+ console.log(chalk_1.default.green(` ✓ Executed: ${workflowName}`));
2281
+ console.log(chalk_1.default.white(` Execution ID: ${result.executionId}`));
2282
+ console.log(chalk_1.default.white(` Async: ${result.isAsync}`));
2283
+ console.log(chalk_1.default.white(` Type: ${result.workflowType || 'standard'}`));
2284
+ if (result.outputs && Object.keys(result.outputs).length > 0) {
2285
+ console.log(chalk_1.default.white(` Outputs:`));
2286
+ console.log(chalk_1.default.gray(` ${JSON.stringify(result.outputs, null, 2).split('\n').join('\n ')}`));
2287
+ }
2288
+ console.log('');
2289
+ }
2290
+ async function runWorkflowExecutions(workflowIdOrFile, orgOverride) {
2291
+ if (!workflowIdOrFile) {
2292
+ console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
2293
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow executions <workflowId|file.yaml> [--org <id>]`));
2294
+ process.exit(2);
2295
+ }
2296
+ const session = resolveSession();
2297
+ const domain = session.domain;
2298
+ const token = session.access_token;
2299
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2300
+ // Resolve workflowId — accept UUID directly or extract from YAML file
2301
+ let workflowId = workflowIdOrFile;
2302
+ if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
2303
+ if (!fs.existsSync(workflowIdOrFile)) {
2304
+ console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
2305
+ process.exit(2);
2306
+ }
2307
+ const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
2308
+ workflowId = parsed?.workflow?.workflowId;
2309
+ if (!workflowId) {
2310
+ console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2311
+ process.exit(2);
2312
+ }
2313
+ }
2314
+ const data = await graphqlRequest(domain, token, `
2315
+ query ($organizationId: Int!, $workflowId: UUID!) {
2316
+ workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 25) {
2317
+ totalCount
2318
+ items { executionId executionStatus executedAt durationMs userId }
2319
+ }
2320
+ }
2321
+ `, { organizationId: orgId, workflowId });
2322
+ const items = data?.workflowExecutions?.items || [];
2323
+ const total = data?.workflowExecutions?.totalCount || 0;
2324
+ console.log(chalk_1.default.bold.cyan('\n Workflow Executions\n'));
2325
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2326
+ console.log(chalk_1.default.gray(` Workflow: ${workflowId}`));
2327
+ console.log(chalk_1.default.gray(` Total: ${total}\n`));
2328
+ if (items.length === 0) {
2329
+ console.log(chalk_1.default.gray(' No executions found.\n'));
2330
+ return;
2331
+ }
2332
+ // Sort descending by executedAt (API may not support ordering)
2333
+ items.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime());
2334
+ for (const ex of items) {
2335
+ const date = new Date(ex.executedAt).toLocaleString();
2336
+ const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
2337
+ const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
2338
+ console.log(chalk_1.default.white(` ${ex.executionId} ${statusColor(ex.executionStatus.padEnd(10))} ${date} ${chalk_1.default.gray(duration)}`));
2339
+ }
2340
+ console.log('');
2341
+ }
2342
+ function fetchGzipText(url) {
2343
+ const zlib = require('zlib');
2344
+ return new Promise((resolve, reject) => {
2345
+ const lib = url.startsWith('https') ? https : http;
2346
+ lib.get(url, (res) => {
2347
+ if (res.statusCode !== 200) {
2348
+ reject(new Error(`HTTP ${res.statusCode}`));
2349
+ res.resume();
2350
+ return;
2351
+ }
2352
+ const rawChunks = [];
2353
+ res.on('data', (chunk) => rawChunks.push(chunk));
2354
+ res.on('end', () => {
2355
+ const raw = Buffer.concat(rawChunks);
2356
+ if (raw.length === 0) {
2357
+ resolve('(empty log)');
2358
+ return;
2359
+ }
2360
+ zlib.gunzip(raw, (err, result) => {
2361
+ if (err) {
2362
+ resolve(raw.toString('utf-8'));
2363
+ return;
2364
+ } // fallback: maybe not gzipped
2365
+ resolve(result.toString('utf-8'));
2366
+ });
2367
+ });
2368
+ }).on('error', reject);
2369
+ });
2370
+ }
2371
+ async function runWorkflowLogs(executionId, orgOverride) {
2372
+ if (!executionId) {
2373
+ console.error(chalk_1.default.red('Error: Execution ID required'));
2374
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow logs <executionId> [--org <id>]`));
2375
+ process.exit(2);
2376
+ }
2377
+ const session = resolveSession();
2378
+ const { domain, access_token: token } = session;
2379
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2380
+ // Fetch execution details with log URLs
2381
+ const data = await graphqlRequest(domain, token, `
2382
+ query ($organizationId: Int!, $executionId: UUID!) {
2383
+ workflowExecution(organizationId: $organizationId, executionId: $executionId) {
2384
+ executionId workflowId executionStatus executedAt durationMs
2385
+ txtLogUrl jsonLogUrl
2386
+ user { fullName email }
2387
+ }
2388
+ }
2389
+ `, { organizationId: orgId, executionId });
2390
+ const ex = data?.workflowExecution;
2391
+ if (!ex) {
2392
+ console.error(chalk_1.default.red(`Execution not found: ${executionId}`));
2393
+ process.exit(2);
2394
+ }
2395
+ const date = new Date(ex.executedAt).toLocaleString();
2396
+ const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
2397
+ const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
2398
+ const userName = ex.user?.fullName || ex.user?.email || '';
2399
+ console.log(chalk_1.default.bold.cyan('\n Workflow Execution\n'));
2400
+ console.log(chalk_1.default.white(` ID: ${ex.executionId}`));
2401
+ console.log(chalk_1.default.white(` Workflow: ${ex.workflowId}`));
2402
+ console.log(chalk_1.default.white(` Status: ${statusColor(ex.executionStatus)}`));
2403
+ console.log(chalk_1.default.white(` Executed: ${date}`));
2404
+ console.log(chalk_1.default.white(` Duration: ${duration}`));
2405
+ if (userName)
2406
+ console.log(chalk_1.default.white(` User: ${userName}`));
2407
+ // Download and display the text log
2408
+ if (ex.txtLogUrl) {
2409
+ try {
2410
+ const logText = await fetchGzipText(ex.txtLogUrl);
2411
+ console.log(chalk_1.default.gray('\n --- Log ---\n'));
2412
+ console.log(logText);
2413
+ }
2414
+ catch (e) {
2415
+ console.log(chalk_1.default.yellow(`\n Failed to download log: ${e.message}`));
2416
+ console.log(chalk_1.default.gray(` txtLogUrl: ${ex.txtLogUrl}\n`));
2417
+ }
2418
+ }
2419
+ else {
2420
+ console.log(chalk_1.default.gray('\n No log available for this execution.\n'));
2421
+ }
2422
+ }
2423
+ // ============================================================================
2424
+ // Publish Command
2425
+ // ============================================================================
2426
+ async function pushWorkflowQuiet(domain, token, orgId, file) {
2427
+ let name = path.basename(file);
2428
+ try {
2429
+ const yamlContent = fs.readFileSync(file, 'utf-8');
2430
+ const parsed = yaml_1.default.parse(yamlContent);
2431
+ const workflowId = parsed?.workflow?.workflowId;
2432
+ name = parsed?.workflow?.name || name;
2433
+ if (!workflowId)
2434
+ return { ok: false, name, error: 'Missing workflow.workflowId' };
2435
+ const checkData = await graphqlRequest(domain, token, `
2436
+ query ($organizationId: Int!, $workflowId: UUID!) {
2437
+ workflow(organizationId: $organizationId, workflowId: $workflowId) { workflowId }
2438
+ }
2439
+ `, { organizationId: orgId, workflowId });
2440
+ if (checkData?.workflow) {
2441
+ await graphqlRequest(domain, token, `
2442
+ mutation ($input: UpdateWorkflowInput!) {
2443
+ updateWorkflow(input: $input) { workflow { workflowId } }
2444
+ }
2445
+ `, { input: { organizationId: orgId, workflowId, workflowYamlDocument: yamlContent } });
2446
+ }
2447
+ else {
2448
+ await graphqlRequest(domain, token, `
2449
+ mutation ($input: CreateWorkflowInput!) {
2450
+ createWorkflow(input: $input) { workflow { workflowId } }
2451
+ }
2452
+ `, { input: { organizationId: orgId, workflowYamlDocument: yamlContent } });
2453
+ }
2454
+ return { ok: true, name };
2455
+ }
2456
+ catch (e) {
2457
+ return { ok: false, name, error: e.message };
2458
+ }
2459
+ }
2460
+ async function pushModuleQuiet(domain, token, orgId, file, appManifestId) {
2461
+ let name = path.basename(file);
2462
+ try {
2463
+ const yamlContent = fs.readFileSync(file, 'utf-8');
2464
+ const parsed = yaml_1.default.parse(yamlContent);
2465
+ const appModuleId = parsed?.module?.appModuleId;
2466
+ name = parsed?.module?.name || name;
2467
+ if (!appModuleId)
2468
+ return { ok: false, name, error: 'Missing module.appModuleId' };
2469
+ const checkData = await graphqlRequest(domain, token, `
2470
+ query ($organizationId: Int!, $appModuleId: UUID!) {
2471
+ appModule(organizationId: $organizationId, appModuleId: $appModuleId) { appModuleId }
2472
+ }
2473
+ `, { organizationId: orgId, appModuleId });
2474
+ if (checkData?.appModule) {
2475
+ await graphqlRequest(domain, token, `
2476
+ mutation ($input: UpdateAppModuleInput!) {
2477
+ updateAppModule(input: $input) { appModule { appModuleId name } }
2478
+ }
2479
+ `, { input: { organizationId: orgId, appModuleId, values: { appModuleYamlDocument: yamlContent } } });
2480
+ }
2481
+ else {
2482
+ const values = { appModuleYamlDocument: yamlContent };
2483
+ if (appManifestId)
2484
+ values.appManifestId = appManifestId;
2485
+ await graphqlRequest(domain, token, `
2486
+ mutation ($input: CreateAppModuleInput!) {
2487
+ createAppModule(input: $input) { appModule { appModuleId name } }
2488
+ }
2489
+ `, { input: { organizationId: orgId, values } });
2490
+ }
2491
+ return { ok: true, name };
2492
+ }
2493
+ catch (e) {
2494
+ return { ok: false, name, error: e.message };
2495
+ }
2496
+ }
2497
+ // ============================================================================
2498
+ // PAT Token Commands
2499
+ // ============================================================================
2500
+ async function runPatCreate(name) {
2501
+ const session = resolveSession();
2502
+ const { domain, access_token: token } = session;
2503
+ const data = await graphqlRequest(domain, token, `
2504
+ mutation ($input: CreatePersonalAccessTokenInput!) {
2505
+ createPersonalAccessToken(input: $input) {
2506
+ createPatPayload {
2507
+ token
2508
+ personalAccessToken { id name scopes }
2509
+ }
2510
+ }
2511
+ }
2512
+ `, { input: { input: { name, scopes: ['TMS.ApiAPI'] } } });
2513
+ const payload = data?.createPersonalAccessToken?.createPatPayload;
2514
+ const patToken = payload?.token;
2515
+ const pat = payload?.personalAccessToken;
2516
+ if (!patToken) {
2517
+ console.error(chalk_1.default.red('Failed to create PAT token — no token returned.'));
2518
+ process.exit(2);
2519
+ }
2520
+ console.log(chalk_1.default.green('PAT token created successfully!'));
2521
+ console.log();
2522
+ console.log(chalk_1.default.bold(' Token:'), chalk_1.default.cyan(patToken));
2523
+ console.log(chalk_1.default.bold(' ID: '), chalk_1.default.gray(pat?.id || 'unknown'));
2524
+ console.log(chalk_1.default.bold(' Name: '), pat?.name || name);
2525
+ console.log();
2526
+ console.log(chalk_1.default.yellow('⚠ Copy the token now — it will not be shown again.'));
2527
+ console.log();
2528
+ console.log(chalk_1.default.bold('To use PAT authentication, add to your project .env file:'));
2529
+ console.log();
2530
+ console.log(chalk_1.default.cyan(` CXTMS_AUTH=${patToken}`));
2531
+ console.log(chalk_1.default.cyan(` CXTMS_SERVER=${domain}`));
2532
+ console.log();
2533
+ console.log(chalk_1.default.gray('When CXTMS_AUTH is set, cx-cli will skip OAuth login and use the PAT token directly.'));
2534
+ console.log(chalk_1.default.gray('You can also export these as environment variables instead of using .env.'));
2535
+ }
2536
+ async function runPatList() {
2537
+ const session = resolveSession();
2538
+ const { domain, access_token: token } = session;
2539
+ const data = await graphqlRequest(domain, token, `
2540
+ {
2541
+ personalAccessTokens(skip: 0, take: 50) {
2542
+ items { id name createdAt expiresAt lastUsedAt scopes }
2543
+ totalCount
2544
+ }
2545
+ }
2546
+ `, {});
2547
+ const items = data?.personalAccessTokens?.items || [];
2548
+ const total = data?.personalAccessTokens?.totalCount ?? items.length;
2549
+ if (items.length === 0) {
2550
+ console.log(chalk_1.default.gray('No active PAT tokens found.'));
2551
+ return;
2552
+ }
2553
+ console.log(chalk_1.default.bold(`PAT tokens (${total}):\n`));
2554
+ for (const t of items) {
2555
+ const expires = t.expiresAt ? new Date(t.expiresAt).toLocaleDateString() : 'never';
2556
+ const lastUsed = t.lastUsedAt ? new Date(t.lastUsedAt).toLocaleDateString() : 'never';
2557
+ console.log(` ${chalk_1.default.cyan(t.name || '(unnamed)')}`);
2558
+ console.log(` ID: ${chalk_1.default.gray(t.id)}`);
2559
+ console.log(` Created: ${new Date(t.createdAt).toLocaleDateString()}`);
2560
+ console.log(` Expires: ${expires}`);
2561
+ console.log(` Last used: ${lastUsed}`);
2562
+ console.log(` Scopes: ${(t.scopes || []).join(', ') || 'none'}`);
2563
+ console.log();
2564
+ }
2565
+ }
2566
+ async function runPatRevoke(id) {
2567
+ const session = resolveSession();
2568
+ const { domain, access_token: token } = session;
2569
+ const data = await graphqlRequest(domain, token, `
2570
+ mutation ($input: RevokePersonalAccessTokenInput!) {
2571
+ revokePersonalAccessToken(input: $input) {
2572
+ personalAccessToken { id name revokedAt }
2573
+ }
2574
+ }
2575
+ `, { input: { id } });
2576
+ const revoked = data?.revokePersonalAccessToken?.personalAccessToken;
2577
+ if (revoked) {
2578
+ console.log(chalk_1.default.green(`PAT token revoked: ${revoked.name || revoked.id}`));
2579
+ }
2580
+ else {
2581
+ console.log(chalk_1.default.green('PAT token revoked.'));
2582
+ }
2583
+ }
2584
+ async function runPatSetup() {
2585
+ const patToken = process.env.CXTMS_AUTH;
2586
+ const server = process.env.CXTMS_SERVER || resolveDomainFromAppYaml();
2587
+ console.log(chalk_1.default.bold('PAT Token Status:\n'));
2588
+ if (patToken) {
2589
+ const masked = patToken.slice(0, 8) + '...' + patToken.slice(-4);
2590
+ console.log(chalk_1.default.green(` CXTMS_AUTH is set: ${masked}`));
2591
+ }
2592
+ else {
2593
+ console.log(chalk_1.default.yellow(' CXTMS_AUTH is not set'));
2594
+ }
2595
+ if (server) {
2596
+ console.log(chalk_1.default.green(` Server: ${server}`));
2597
+ }
2598
+ else {
2599
+ console.log(chalk_1.default.yellow(' Server: not configured (add `server` to app.yaml or set CXTMS_SERVER)'));
2600
+ }
2601
+ console.log();
2602
+ if (patToken && server) {
2603
+ console.log(chalk_1.default.green('PAT authentication is active. OAuth login will be skipped.'));
2604
+ }
2605
+ else {
2606
+ console.log(chalk_1.default.bold('To set up PAT authentication:'));
2607
+ console.log();
2608
+ console.log(chalk_1.default.white(' 1. Create a token:'));
2609
+ console.log(chalk_1.default.cyan(' cx-cli pat create "my-token-name"'));
2610
+ console.log();
2611
+ console.log(chalk_1.default.white(' 2. Add to your project .env file:'));
2612
+ console.log(chalk_1.default.cyan(' CXTMS_AUTH=pat_xxxxx'));
2613
+ console.log(chalk_1.default.cyan(' CXTMS_SERVER=https://your-server.com'));
2614
+ console.log();
2615
+ console.log(chalk_1.default.gray(' Or set `server` in app.yaml instead of CXTMS_SERVER.'));
2616
+ }
2617
+ }
2618
+ async function runPublish(featureDir, orgOverride) {
2619
+ const session = resolveSession();
2620
+ const domain = session.domain;
2621
+ const token = session.access_token;
2622
+ const orgId = await resolveOrgId(domain, token, orgOverride);
2623
+ // Read app.yaml
2624
+ const appYamlPath = path.join(process.cwd(), 'app.yaml');
2625
+ if (!fs.existsSync(appYamlPath)) {
2626
+ console.error(chalk_1.default.red('Error: app.yaml not found in current directory'));
2627
+ process.exit(2);
2628
+ }
2629
+ const appYaml = yaml_1.default.parse(fs.readFileSync(appYamlPath, 'utf-8'));
2630
+ const appManifestId = appYaml?.id;
2631
+ const appName = appYaml?.name || 'unknown';
2632
+ console.log(chalk_1.default.bold.cyan('\n Publish\n'));
2633
+ console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2634
+ console.log(chalk_1.default.gray(` Org: ${orgId}`));
2635
+ console.log(chalk_1.default.gray(` App: ${appName}`));
2636
+ if (featureDir) {
2637
+ console.log(chalk_1.default.gray(` Feature: ${featureDir}`));
2638
+ }
2639
+ console.log('');
2640
+ // Step 1: Create or update app manifest
2641
+ if (appManifestId) {
2642
+ console.log(chalk_1.default.gray(' Publishing app manifest...'));
2643
+ try {
2644
+ const checkData = await graphqlRequest(domain, token, `
2645
+ query ($organizationId: Int!, $appManifestId: UUID!) {
2646
+ appManifest(organizationId: $organizationId, appManifestId: $appManifestId) { appManifestId }
2647
+ }
2648
+ `, { organizationId: orgId, appManifestId });
2649
+ if (checkData?.appManifest) {
2650
+ await graphqlRequest(domain, token, `
2651
+ mutation ($input: UpdateAppManifestInput!) {
2652
+ updateAppManifest(input: $input) { appManifest { appManifestId name } }
2653
+ }
2654
+ `, { input: { organizationId: orgId, appManifestId, values: { name: appName, description: appYaml?.description || '' } } });
2655
+ console.log(chalk_1.default.green(' ✓ App manifest updated'));
2656
+ }
2657
+ else {
2658
+ await graphqlRequest(domain, token, `
2659
+ mutation ($input: CreateAppManifestInput!) {
2660
+ createAppManifest(input: $input) { appManifest { appManifestId name } }
2661
+ }
2662
+ `, { input: { organizationId: orgId, values: { appManifestId, name: appName, description: appYaml?.description || '' } } });
2663
+ console.log(chalk_1.default.green(' ✓ App manifest created'));
2664
+ }
2665
+ }
2666
+ catch (e) {
2667
+ console.log(chalk_1.default.red(` ✗ App manifest failed: ${e.message}`));
2668
+ }
2669
+ }
2670
+ // Step 2: Discover files
2671
+ const baseDir = featureDir ? path.join(process.cwd(), 'features', featureDir) : process.cwd();
2672
+ if (featureDir && !fs.existsSync(baseDir)) {
2673
+ console.error(chalk_1.default.red(`Error: Feature directory not found: features/${featureDir}`));
2674
+ process.exit(2);
2675
+ }
2676
+ const workflowDirs = [path.join(baseDir, 'workflows')];
2677
+ const moduleDirs = [path.join(baseDir, 'modules')];
2678
+ // Collect YAML files
2679
+ const workflowFiles = [];
2680
+ const moduleFiles = [];
2681
+ for (const dir of workflowDirs) {
2682
+ if (fs.existsSync(dir)) {
2683
+ for (const f of fs.readdirSync(dir)) {
2684
+ if (f.endsWith('.yaml') || f.endsWith('.yml')) {
2685
+ workflowFiles.push(path.join(dir, f));
2686
+ }
2687
+ }
2688
+ }
2689
+ }
2690
+ for (const dir of moduleDirs) {
2691
+ if (fs.existsSync(dir)) {
2692
+ for (const f of fs.readdirSync(dir)) {
2693
+ if (f.endsWith('.yaml') || f.endsWith('.yml')) {
2694
+ moduleFiles.push(path.join(dir, f));
2695
+ }
2696
+ }
2697
+ }
2698
+ }
2699
+ console.log(chalk_1.default.gray(`\n Found ${workflowFiles.length} workflow(s), ${moduleFiles.length} module(s)\n`));
2700
+ let succeeded = 0;
2701
+ let failed = 0;
2702
+ // Step 3: Push workflows
2703
+ for (const file of workflowFiles) {
2704
+ const relPath = path.relative(process.cwd(), file);
2705
+ const result = await pushWorkflowQuiet(domain, token, orgId, file);
2706
+ if (result.ok) {
2707
+ console.log(chalk_1.default.green(` ✓ ${relPath}`));
2708
+ succeeded++;
2709
+ }
2710
+ else {
2711
+ console.log(chalk_1.default.red(` ✗ ${relPath}: ${result.error}`));
2712
+ failed++;
2713
+ }
2714
+ }
2715
+ // Step 4: Push modules
2716
+ for (const file of moduleFiles) {
2717
+ const relPath = path.relative(process.cwd(), file);
2718
+ const result = await pushModuleQuiet(domain, token, orgId, file, appManifestId);
2719
+ if (result.ok) {
2720
+ console.log(chalk_1.default.green(` ✓ ${relPath}`));
2721
+ succeeded++;
2722
+ }
2723
+ else {
2724
+ console.log(chalk_1.default.red(` ✗ ${relPath}: ${result.error}`));
2725
+ failed++;
2726
+ }
2727
+ }
2728
+ // Summary
2729
+ console.log('');
2730
+ if (failed === 0) {
2731
+ console.log(chalk_1.default.green(` ✓ Published ${succeeded} file(s) successfully\n`));
2732
+ }
2733
+ else {
2734
+ console.log(chalk_1.default.yellow(` Published ${succeeded} file(s), ${failed} failed\n`));
2735
+ }
2736
+ }
2737
+ // ============================================================================
1341
2738
  // Extract Command
1342
2739
  // ============================================================================
1343
2740
  function runExtract(sourceFile, componentName, targetFile, copy) {
@@ -1548,7 +2945,7 @@ function parseArgs(args) {
1548
2945
  reportFormat: 'json'
1549
2946
  };
1550
2947
  // Check for commands
1551
- const commands = ['validate', 'schema', 'example', 'list', 'help', 'version', 'report', 'init', 'create', 'extract', 'sync-schemas', 'install-skills', 'update', 'setup-claude'];
2948
+ 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'];
1552
2949
  if (args.length > 0 && commands.includes(args[0])) {
1553
2950
  command = args[0];
1554
2951
  args = args.slice(1);
@@ -1624,6 +3021,18 @@ function parseArgs(args) {
1624
3021
  else if (arg === '--copy') {
1625
3022
  options.extractCopy = true;
1626
3023
  }
3024
+ else if (arg === '--org') {
3025
+ const orgArg = args[++i];
3026
+ const parsed = parseInt(orgArg, 10);
3027
+ if (isNaN(parsed)) {
3028
+ console.error(chalk_1.default.red(`Invalid --org value: ${orgArg}. Must be a number.`));
3029
+ process.exit(2);
3030
+ }
3031
+ options.orgId = parsed;
3032
+ }
3033
+ else if (arg === '--vars') {
3034
+ options.vars = args[++i];
3035
+ }
1627
3036
  else if (!arg.startsWith('-')) {
1628
3037
  files.push(arg);
1629
3038
  }
@@ -2413,6 +3822,118 @@ async function main() {
2413
3822
  console.log(`cx-cli v${VERSION}`);
2414
3823
  process.exit(0);
2415
3824
  }
3825
+ // Handle login command (no schemas needed)
3826
+ if (command === 'login') {
3827
+ if (!files[0]) {
3828
+ console.error(chalk_1.default.red('Error: URL required'));
3829
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} login <url>`));
3830
+ process.exit(2);
3831
+ }
3832
+ await runLogin(files[0]);
3833
+ process.exit(0);
3834
+ }
3835
+ // Handle logout command (no schemas needed)
3836
+ if (command === 'logout') {
3837
+ await runLogout(files[0]);
3838
+ process.exit(0);
3839
+ }
3840
+ // Handle pat command (no schemas needed)
3841
+ if (command === 'pat') {
3842
+ const sub = files[0];
3843
+ if (sub === 'create') {
3844
+ if (!files[1]) {
3845
+ console.error(chalk_1.default.red('Error: Token name required'));
3846
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat create <name>`));
3847
+ process.exit(2);
3848
+ }
3849
+ await runPatCreate(files[1]);
3850
+ }
3851
+ else if (sub === 'list' || !sub) {
3852
+ await runPatList();
3853
+ }
3854
+ else if (sub === 'revoke') {
3855
+ if (!files[1]) {
3856
+ console.error(chalk_1.default.red('Error: Token ID required'));
3857
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat revoke <tokenId>`));
3858
+ process.exit(2);
3859
+ }
3860
+ await runPatRevoke(files[1]);
3861
+ }
3862
+ else if (sub === 'setup') {
3863
+ await runPatSetup();
3864
+ }
3865
+ else {
3866
+ console.error(chalk_1.default.red(`Unknown pat subcommand: ${sub}`));
3867
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} pat <create|list|revoke|setup>`));
3868
+ process.exit(2);
3869
+ }
3870
+ process.exit(0);
3871
+ }
3872
+ // Handle orgs command (no schemas needed)
3873
+ if (command === 'orgs') {
3874
+ const sub = files[0];
3875
+ if (sub === 'list' || !sub) {
3876
+ await runOrgsList();
3877
+ }
3878
+ else if (sub === 'use') {
3879
+ await runOrgsUse(files[1]);
3880
+ }
3881
+ else if (sub === 'select') {
3882
+ await runOrgsSelect();
3883
+ }
3884
+ else {
3885
+ console.error(chalk_1.default.red(`Unknown orgs subcommand: ${sub}`));
3886
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} orgs <list|use|select>`));
3887
+ process.exit(2);
3888
+ }
3889
+ process.exit(0);
3890
+ }
3891
+ // Handle appmodule command (no schemas needed)
3892
+ if (command === 'appmodule') {
3893
+ const sub = files[0];
3894
+ if (sub === 'push') {
3895
+ await runAppModulePush(files[1], options.orgId);
3896
+ }
3897
+ else if (sub === 'delete') {
3898
+ await runAppModuleDelete(files[1], options.orgId);
3899
+ }
3900
+ else {
3901
+ console.error(chalk_1.default.red(`Unknown appmodule subcommand: ${sub || '(none)'}`));
3902
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} appmodule <push|delete> ...`));
3903
+ process.exit(2);
3904
+ }
3905
+ process.exit(0);
3906
+ }
3907
+ // Handle workflow command (no schemas needed)
3908
+ if (command === 'workflow') {
3909
+ const sub = files[0];
3910
+ if (sub === 'push') {
3911
+ await runWorkflowPush(files[1], options.orgId);
3912
+ }
3913
+ else if (sub === 'delete') {
3914
+ await runWorkflowDelete(files[1], options.orgId);
3915
+ }
3916
+ else if (sub === 'execute') {
3917
+ await runWorkflowExecute(files[1], options.orgId, options.vars);
3918
+ }
3919
+ else if (sub === 'executions') {
3920
+ await runWorkflowExecutions(files[1], options.orgId);
3921
+ }
3922
+ else if (sub === 'logs') {
3923
+ await runWorkflowLogs(files[1], options.orgId);
3924
+ }
3925
+ else {
3926
+ console.error(chalk_1.default.red(`Unknown workflow subcommand: ${sub || '(none)'}`));
3927
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <push|delete|execute|executions|logs> ...`));
3928
+ process.exit(2);
3929
+ }
3930
+ process.exit(0);
3931
+ }
3932
+ // Handle publish command (no schemas needed)
3933
+ if (command === 'publish') {
3934
+ await runPublish(files[0] || options.feature, options.orgId);
3935
+ process.exit(0);
3936
+ }
2416
3937
  // Find schemas path
2417
3938
  const schemasPath = options.schemasPath || findSchemasPath();
2418
3939
  if (!schemasPath) {