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