@cxtms/cx-schema 1.6.0 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/cx-core/SKILL.md +85 -0
- package/.claude/skills/cx-module/SKILL.md +26 -0
- package/.claude/skills/cx-workflow/SKILL.md +66 -3
- package/dist/cli.js +288 -160
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -115,7 +115,7 @@ ${chalk_1.default.bold.yellow('COMMANDS:')}
|
|
|
115
115
|
${chalk_1.default.green('pat')} Manage personal access tokens (create, list, revoke)
|
|
116
116
|
${chalk_1.default.green('orgs')} List, select, or set active organization
|
|
117
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,
|
|
118
|
+
${chalk_1.default.green('workflow')} Manage workflows on a CX server (push, delete, execute, logs, log)
|
|
119
119
|
${chalk_1.default.green('publish')} Publish all modules and workflows to a CX server
|
|
120
120
|
${chalk_1.default.green('schema')} Show JSON schema for a component or task
|
|
121
121
|
${chalk_1.default.green('example')} Show example YAML for a component or task
|
|
@@ -139,7 +139,13 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
|
|
|
139
139
|
${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
|
|
140
140
|
${chalk_1.default.green('--to <file>')} Target file for extract command
|
|
141
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
|
|
142
|
+
${chalk_1.default.green('--org <id>')} Organization ID for server commands
|
|
143
|
+
${chalk_1.default.green('--vars <json>')} JSON variables for workflow execute
|
|
144
|
+
${chalk_1.default.green('--from <date>')} Filter logs from date (YYYY-MM-DD)
|
|
145
|
+
${chalk_1.default.green('--to <date>')} Filter logs to date (YYYY-MM-DD)
|
|
146
|
+
${chalk_1.default.green('--output <file>')} Save workflow log to file (or -o)
|
|
147
|
+
${chalk_1.default.green('--console')} Print workflow log to stdout
|
|
148
|
+
${chalk_1.default.green('--json')} Download JSON log instead of text
|
|
143
149
|
|
|
144
150
|
${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
|
|
145
151
|
${chalk_1.default.gray('# Validate a module YAML file')}
|
|
@@ -209,10 +215,7 @@ ${chalk_1.default.bold.yellow('AUTH COMMANDS:')}
|
|
|
209
215
|
${chalk_1.default.gray('# Login to a CX environment')}
|
|
210
216
|
${chalk_1.default.cyan(`${PROGRAM_NAME} login https://qa.storevista.acuitive.net`)}
|
|
211
217
|
|
|
212
|
-
${chalk_1.default.gray('# Logout from
|
|
213
|
-
${chalk_1.default.cyan(`${PROGRAM_NAME} logout https://qa.storevista.acuitive.net`)}
|
|
214
|
-
|
|
215
|
-
${chalk_1.default.gray('# List stored sessions')}
|
|
218
|
+
${chalk_1.default.gray('# Logout from current session')}
|
|
216
219
|
${chalk_1.default.cyan(`${PROGRAM_NAME} logout`)}
|
|
217
220
|
|
|
218
221
|
${chalk_1.default.bold.yellow('PAT COMMANDS:')}
|
|
@@ -258,12 +261,20 @@ ${chalk_1.default.bold.yellow('WORKFLOW COMMANDS:')}
|
|
|
258
261
|
${chalk_1.default.gray('# Delete a workflow by UUID')}
|
|
259
262
|
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow delete <workflowId>`)}
|
|
260
263
|
|
|
261
|
-
${chalk_1.default.gray('#
|
|
262
|
-
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow
|
|
263
|
-
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow
|
|
264
|
+
${chalk_1.default.gray('# Execute a workflow')}
|
|
265
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId|file.yaml>`)}
|
|
266
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId> --vars '{"city":"London"}'`)}
|
|
267
|
+
|
|
268
|
+
${chalk_1.default.gray('# List execution logs for a workflow (sorted desc)')}
|
|
269
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <workflowId|file.yaml>`)}
|
|
270
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <workflowId> --from 2026-01-01 --to 2026-01-31`)}
|
|
264
271
|
|
|
265
|
-
${chalk_1.default.gray('#
|
|
266
|
-
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow
|
|
272
|
+
${chalk_1.default.gray('# Download a specific execution log')}
|
|
273
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId>`)} ${chalk_1.default.gray('# save txt log to temp dir')}
|
|
274
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --output log.txt`)} ${chalk_1.default.gray('# save to file')}
|
|
275
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --console`)} ${chalk_1.default.gray('# print to stdout')}
|
|
276
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --json`)} ${chalk_1.default.gray('# download JSON log (more detail)')}
|
|
277
|
+
${chalk_1.default.cyan(`${PROGRAM_NAME} workflow log <executionId> --json --console`)} ${chalk_1.default.gray('# JSON log to stdout')}
|
|
267
278
|
|
|
268
279
|
${chalk_1.default.bold.yellow('PUBLISH COMMANDS:')}
|
|
269
280
|
${chalk_1.default.gray('# Publish all modules and workflows from current project')}
|
|
@@ -1454,28 +1465,33 @@ function runSetupClaude() {
|
|
|
1454
1465
|
// ============================================================================
|
|
1455
1466
|
const AUTH_CALLBACK_PORT = 9000;
|
|
1456
1467
|
const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
|
|
1457
|
-
function
|
|
1458
|
-
|
|
1468
|
+
function getSessionDir() {
|
|
1469
|
+
const projectName = path.basename(process.cwd());
|
|
1470
|
+
return path.join(os.homedir(), '.cxtms', projectName);
|
|
1459
1471
|
}
|
|
1460
|
-
function
|
|
1461
|
-
|
|
1462
|
-
return path.join(getTokenDir(), `${hostname}.json`);
|
|
1472
|
+
function getSessionFilePath() {
|
|
1473
|
+
return path.join(getSessionDir(), '.session.json');
|
|
1463
1474
|
}
|
|
1464
|
-
function
|
|
1465
|
-
const filePath =
|
|
1475
|
+
function readSessionFile() {
|
|
1476
|
+
const filePath = getSessionFilePath();
|
|
1466
1477
|
if (!fs.existsSync(filePath))
|
|
1467
1478
|
return null;
|
|
1468
|
-
|
|
1479
|
+
try {
|
|
1480
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1481
|
+
}
|
|
1482
|
+
catch {
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1469
1485
|
}
|
|
1470
|
-
function
|
|
1471
|
-
const dir =
|
|
1486
|
+
function writeSessionFile(data) {
|
|
1487
|
+
const dir = getSessionDir();
|
|
1472
1488
|
if (!fs.existsSync(dir)) {
|
|
1473
1489
|
fs.mkdirSync(dir, { recursive: true });
|
|
1474
1490
|
}
|
|
1475
|
-
fs.writeFileSync(
|
|
1491
|
+
fs.writeFileSync(getSessionFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
1476
1492
|
}
|
|
1477
|
-
function
|
|
1478
|
-
const filePath =
|
|
1493
|
+
function deleteSessionFile() {
|
|
1494
|
+
const filePath = getSessionFilePath();
|
|
1479
1495
|
if (fs.existsSync(filePath)) {
|
|
1480
1496
|
fs.unlinkSync(filePath);
|
|
1481
1497
|
}
|
|
@@ -1615,7 +1631,7 @@ async function refreshTokens(stored) {
|
|
|
1615
1631
|
refresh_token: data.refresh_token || stored.refresh_token,
|
|
1616
1632
|
expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
|
|
1617
1633
|
};
|
|
1618
|
-
|
|
1634
|
+
writeSessionFile(updated);
|
|
1619
1635
|
return updated;
|
|
1620
1636
|
}
|
|
1621
1637
|
async function runLogin(domain) {
|
|
@@ -1659,51 +1675,30 @@ async function runLogin(domain) {
|
|
|
1659
1675
|
// Step 6: Exchange code for tokens
|
|
1660
1676
|
console.log(chalk_1.default.gray(' Exchanging authorization code...'));
|
|
1661
1677
|
const tokens = await exchangeCodeForTokens(domain, clientId, code, codeVerifier);
|
|
1662
|
-
// Step 7: Store
|
|
1663
|
-
|
|
1678
|
+
// Step 7: Store session locally
|
|
1679
|
+
writeSessionFile(tokens);
|
|
1664
1680
|
close();
|
|
1665
1681
|
console.log(chalk_1.default.green(` ✓ Logged in to ${new URL(domain).hostname}`));
|
|
1666
|
-
console.log(chalk_1.default.gray(`
|
|
1667
|
-
}
|
|
1668
|
-
async function runLogout(
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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`));
|
|
1682
|
+
console.log(chalk_1.default.gray(` Session stored at: ${getSessionFilePath()}\n`));
|
|
1683
|
+
}
|
|
1684
|
+
async function runLogout(_domain) {
|
|
1685
|
+
const session = readSessionFile();
|
|
1686
|
+
if (!session) {
|
|
1687
|
+
console.log(chalk_1.default.gray('\n No active session in this project.\n'));
|
|
1688
|
+
console.log(chalk_1.default.gray(` Login first: ${PROGRAM_NAME} login <url>\n`));
|
|
1697
1689
|
return;
|
|
1698
1690
|
}
|
|
1699
1691
|
console.log(chalk_1.default.bold.cyan('\n CX CLI Logout\n'));
|
|
1692
|
+
console.log(chalk_1.default.gray(` Server: ${new URL(session.domain).hostname}`));
|
|
1700
1693
|
// Revoke tokens (non-fatal)
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1694
|
+
if (session.client_id && session.refresh_token) {
|
|
1695
|
+
console.log(chalk_1.default.gray(' Revoking tokens...'));
|
|
1696
|
+
await revokeToken(session.domain, session.client_id, session.access_token);
|
|
1697
|
+
await revokeToken(session.domain, session.client_id, session.refresh_token);
|
|
1698
|
+
}
|
|
1699
|
+
// Delete local session file
|
|
1700
|
+
deleteSessionFile();
|
|
1701
|
+
console.log(chalk_1.default.green(` ✓ Logged out from ${new URL(session.domain).hostname}\n`));
|
|
1707
1702
|
}
|
|
1708
1703
|
// ============================================================================
|
|
1709
1704
|
// AppModule Commands
|
|
@@ -1716,7 +1711,7 @@ async function graphqlRequest(domain, token, query, variables) {
|
|
|
1716
1711
|
if (process.env.CXTMS_AUTH)
|
|
1717
1712
|
throw new Error('PAT token unauthorized (401). Check your CXTMS_AUTH token.');
|
|
1718
1713
|
// Try refresh for OAuth sessions
|
|
1719
|
-
const stored =
|
|
1714
|
+
const stored = readSessionFile();
|
|
1720
1715
|
if (!stored)
|
|
1721
1716
|
throw new Error('Session expired. Run `cx-cli login <url>` again.');
|
|
1722
1717
|
try {
|
|
@@ -1808,43 +1803,20 @@ function resolveSession() {
|
|
|
1808
1803
|
expires_at: 0,
|
|
1809
1804
|
};
|
|
1810
1805
|
}
|
|
1811
|
-
// 1. Check
|
|
1812
|
-
const
|
|
1813
|
-
if (
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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.'));
|
|
1806
|
+
// 1. Check local .cxtms/.session.json
|
|
1807
|
+
const session = readSessionFile();
|
|
1808
|
+
if (session)
|
|
1809
|
+
return session;
|
|
1810
|
+
// 2. Not logged in
|
|
1811
|
+
console.error(chalk_1.default.red('Not logged in. Run `cx-cli login <url>` first.'));
|
|
1840
1812
|
process.exit(2);
|
|
1841
1813
|
}
|
|
1842
1814
|
async function resolveOrgId(domain, token, override) {
|
|
1843
1815
|
// 1. Explicit override
|
|
1844
1816
|
if (override !== undefined)
|
|
1845
1817
|
return override;
|
|
1846
|
-
// 2. Cached in
|
|
1847
|
-
const stored =
|
|
1818
|
+
// 2. Cached in session file
|
|
1819
|
+
const stored = readSessionFile();
|
|
1848
1820
|
if (stored?.organization_id)
|
|
1849
1821
|
return stored.organization_id;
|
|
1850
1822
|
// 3. Query server
|
|
@@ -1860,7 +1832,7 @@ async function resolveOrgId(domain, token, override) {
|
|
|
1860
1832
|
// Cache it
|
|
1861
1833
|
if (stored) {
|
|
1862
1834
|
stored.organization_id = orgId;
|
|
1863
|
-
|
|
1835
|
+
writeSessionFile(stored);
|
|
1864
1836
|
}
|
|
1865
1837
|
return orgId;
|
|
1866
1838
|
}
|
|
@@ -2068,9 +2040,9 @@ async function runOrgsUse(orgIdStr) {
|
|
|
2068
2040
|
console.error('');
|
|
2069
2041
|
process.exit(2);
|
|
2070
2042
|
}
|
|
2071
|
-
// Save to
|
|
2043
|
+
// Save to session file
|
|
2072
2044
|
session.organization_id = orgId;
|
|
2073
|
-
|
|
2045
|
+
writeSessionFile(session);
|
|
2074
2046
|
console.log(chalk_1.default.green(`\n ✓ Context set to: ${match.companyName} (${orgId})\n`));
|
|
2075
2047
|
}
|
|
2076
2048
|
async function runOrgsSelect() {
|
|
@@ -2109,7 +2081,7 @@ async function runOrgsSelect() {
|
|
|
2109
2081
|
}
|
|
2110
2082
|
const selected = orgs[idx];
|
|
2111
2083
|
session.organization_id = selected.organizationId;
|
|
2112
|
-
|
|
2084
|
+
writeSessionFile(session);
|
|
2113
2085
|
console.log(chalk_1.default.green(`\n ✓ Context set to: ${selected.companyName} (${selected.organizationId})\n`));
|
|
2114
2086
|
}
|
|
2115
2087
|
// ============================================================================
|
|
@@ -2213,18 +2185,18 @@ async function runWorkflowDelete(uuid, orgOverride) {
|
|
|
2213
2185
|
});
|
|
2214
2186
|
console.log(chalk_1.default.green(` ✓ Deleted: ${uuid}\n`));
|
|
2215
2187
|
}
|
|
2216
|
-
async function
|
|
2188
|
+
async function runWorkflowExecute(workflowIdOrFile, orgOverride, variables) {
|
|
2217
2189
|
if (!workflowIdOrFile) {
|
|
2218
2190
|
console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
|
|
2219
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow
|
|
2191
|
+
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow execute <workflowId|file.yaml> [--org <id>] [--vars '{"key":"value"}']`));
|
|
2220
2192
|
process.exit(2);
|
|
2221
2193
|
}
|
|
2222
2194
|
const session = resolveSession();
|
|
2223
|
-
const domain = session
|
|
2224
|
-
const token = session.access_token;
|
|
2195
|
+
const { domain, access_token: token } = session;
|
|
2225
2196
|
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2226
|
-
// Resolve workflowId
|
|
2197
|
+
// Resolve workflowId
|
|
2227
2198
|
let workflowId = workflowIdOrFile;
|
|
2199
|
+
let workflowName = workflowIdOrFile;
|
|
2228
2200
|
if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
|
|
2229
2201
|
if (!fs.existsSync(workflowIdOrFile)) {
|
|
2230
2202
|
console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
|
|
@@ -2232,54 +2204,181 @@ async function runWorkflowExecutions(workflowIdOrFile, orgOverride) {
|
|
|
2232
2204
|
}
|
|
2233
2205
|
const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
|
|
2234
2206
|
workflowId = parsed?.workflow?.workflowId;
|
|
2207
|
+
workflowName = parsed?.workflow?.name || path.basename(workflowIdOrFile);
|
|
2235
2208
|
if (!workflowId) {
|
|
2236
2209
|
console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
|
|
2237
2210
|
process.exit(2);
|
|
2238
2211
|
}
|
|
2239
2212
|
}
|
|
2213
|
+
// Parse variables if provided
|
|
2214
|
+
let vars;
|
|
2215
|
+
if (variables) {
|
|
2216
|
+
try {
|
|
2217
|
+
vars = JSON.parse(variables);
|
|
2218
|
+
}
|
|
2219
|
+
catch {
|
|
2220
|
+
console.error(chalk_1.default.red('Error: --vars must be valid JSON'));
|
|
2221
|
+
process.exit(2);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
console.log(chalk_1.default.bold.cyan('\n Workflow Execute\n'));
|
|
2225
|
+
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2226
|
+
console.log(chalk_1.default.gray(` Org: ${orgId}`));
|
|
2227
|
+
console.log(chalk_1.default.gray(` Workflow: ${workflowName}`));
|
|
2228
|
+
if (vars)
|
|
2229
|
+
console.log(chalk_1.default.gray(` Variables: ${JSON.stringify(vars)}`));
|
|
2230
|
+
console.log('');
|
|
2231
|
+
const input = { organizationId: orgId, workflowId };
|
|
2232
|
+
if (vars)
|
|
2233
|
+
input.variables = vars;
|
|
2234
|
+
const data = await graphqlRequest(domain, token, `
|
|
2235
|
+
mutation ($input: ExecuteWorkflowInput!) {
|
|
2236
|
+
executeWorkflow(input: $input) {
|
|
2237
|
+
workflowExecutionResult {
|
|
2238
|
+
executionId workflowId isAsync workflowType outputs
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
`, { input });
|
|
2243
|
+
const result = data?.executeWorkflow?.workflowExecutionResult;
|
|
2244
|
+
if (!result) {
|
|
2245
|
+
console.error(chalk_1.default.red(' No execution result returned.\n'));
|
|
2246
|
+
process.exit(2);
|
|
2247
|
+
}
|
|
2248
|
+
console.log(chalk_1.default.green(` ✓ Executed: ${workflowName}`));
|
|
2249
|
+
console.log(chalk_1.default.white(` Execution ID: ${result.executionId}`));
|
|
2250
|
+
console.log(chalk_1.default.white(` Async: ${result.isAsync}`));
|
|
2251
|
+
console.log(chalk_1.default.white(` Type: ${result.workflowType || 'standard'}`));
|
|
2252
|
+
if (result.outputs && Object.keys(result.outputs).length > 0) {
|
|
2253
|
+
console.log(chalk_1.default.white(` Outputs:`));
|
|
2254
|
+
console.log(chalk_1.default.gray(` ${JSON.stringify(result.outputs, null, 2).split('\n').join('\n ')}`));
|
|
2255
|
+
}
|
|
2256
|
+
console.log('');
|
|
2257
|
+
}
|
|
2258
|
+
function resolveWorkflowId(workflowIdOrFile) {
|
|
2259
|
+
if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
|
|
2260
|
+
if (!fs.existsSync(workflowIdOrFile)) {
|
|
2261
|
+
console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
|
|
2262
|
+
process.exit(2);
|
|
2263
|
+
}
|
|
2264
|
+
const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
|
|
2265
|
+
const id = parsed?.workflow?.workflowId;
|
|
2266
|
+
if (!id) {
|
|
2267
|
+
console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
|
|
2268
|
+
process.exit(2);
|
|
2269
|
+
}
|
|
2270
|
+
return id;
|
|
2271
|
+
}
|
|
2272
|
+
return workflowIdOrFile;
|
|
2273
|
+
}
|
|
2274
|
+
async function runWorkflowLogs(workflowIdOrFile, orgOverride, fromDate, toDate) {
|
|
2275
|
+
if (!workflowIdOrFile) {
|
|
2276
|
+
console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
|
|
2277
|
+
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow logs <workflowId|file.yaml> [--from <date>] [--to <date>]`));
|
|
2278
|
+
process.exit(2);
|
|
2279
|
+
}
|
|
2280
|
+
const session = resolveSession();
|
|
2281
|
+
const { domain, access_token: token } = session;
|
|
2282
|
+
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2283
|
+
const workflowId = resolveWorkflowId(workflowIdOrFile);
|
|
2284
|
+
// Parse date filters
|
|
2285
|
+
const fromTs = fromDate ? new Date(fromDate).getTime() : 0;
|
|
2286
|
+
const toTs = toDate ? new Date(toDate + 'T23:59:59').getTime() : Infinity;
|
|
2287
|
+
if (fromDate && isNaN(fromTs)) {
|
|
2288
|
+
console.error(chalk_1.default.red(`Invalid --from date: ${fromDate}. Use YYYY-MM-DD format.`));
|
|
2289
|
+
process.exit(2);
|
|
2290
|
+
}
|
|
2291
|
+
if (toDate && isNaN(toTs)) {
|
|
2292
|
+
console.error(chalk_1.default.red(`Invalid --to date: ${toDate}. Use YYYY-MM-DD format.`));
|
|
2293
|
+
process.exit(2);
|
|
2294
|
+
}
|
|
2240
2295
|
const data = await graphqlRequest(domain, token, `
|
|
2241
2296
|
query ($organizationId: Int!, $workflowId: UUID!) {
|
|
2242
|
-
workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take:
|
|
2297
|
+
workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 100) {
|
|
2243
2298
|
totalCount
|
|
2244
|
-
items { executionId executionStatus executedAt durationMs
|
|
2299
|
+
items { executionId executionStatus executedAt durationMs txtLogUrl user { fullName email } }
|
|
2245
2300
|
}
|
|
2246
2301
|
}
|
|
2247
2302
|
`, { organizationId: orgId, workflowId });
|
|
2248
|
-
|
|
2303
|
+
let items = data?.workflowExecutions?.items || [];
|
|
2249
2304
|
const total = data?.workflowExecutions?.totalCount || 0;
|
|
2250
|
-
|
|
2305
|
+
// Filter by date range
|
|
2306
|
+
if (fromDate || toDate) {
|
|
2307
|
+
items = items.filter((ex) => {
|
|
2308
|
+
const t = new Date(ex.executedAt).getTime();
|
|
2309
|
+
return t >= fromTs && t <= toTs;
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
// Sort descending
|
|
2313
|
+
items.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime());
|
|
2314
|
+
console.log(chalk_1.default.bold.cyan('\n Workflow Logs\n'));
|
|
2251
2315
|
console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
|
|
2252
2316
|
console.log(chalk_1.default.gray(` Workflow: ${workflowId}`));
|
|
2253
|
-
console.log(chalk_1.default.gray(` Total: ${total}
|
|
2317
|
+
console.log(chalk_1.default.gray(` Total: ${total}`));
|
|
2318
|
+
if (fromDate || toDate) {
|
|
2319
|
+
console.log(chalk_1.default.gray(` Filter: ${fromDate || '...'} → ${toDate || '...'}`));
|
|
2320
|
+
}
|
|
2321
|
+
console.log(chalk_1.default.gray(` Showing: ${items.length}\n`));
|
|
2254
2322
|
if (items.length === 0) {
|
|
2255
2323
|
console.log(chalk_1.default.gray(' No executions found.\n'));
|
|
2256
2324
|
return;
|
|
2257
2325
|
}
|
|
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
2326
|
for (const ex of items) {
|
|
2261
2327
|
const date = new Date(ex.executedAt).toLocaleString();
|
|
2262
2328
|
const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
|
|
2263
2329
|
const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
2264
|
-
|
|
2330
|
+
const logIcon = ex.txtLogUrl ? chalk_1.default.green('●') : chalk_1.default.gray('○');
|
|
2331
|
+
const user = ex.user?.fullName || ex.user?.email || '';
|
|
2332
|
+
console.log(` ${logIcon} ${chalk_1.default.white(ex.executionId)} ${statusColor(ex.executionStatus.padEnd(10))} ${date} ${chalk_1.default.gray(duration)}${user ? ' ' + chalk_1.default.gray(user) : ''}`);
|
|
2265
2333
|
}
|
|
2266
|
-
console.log(
|
|
2334
|
+
console.log();
|
|
2335
|
+
console.log(chalk_1.default.gray(` ${chalk_1.default.green('●')} log available ${chalk_1.default.gray('○')} no log`));
|
|
2336
|
+
console.log(chalk_1.default.gray(` Download: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console]\n`));
|
|
2267
2337
|
}
|
|
2268
|
-
|
|
2338
|
+
function fetchGzipText(url) {
|
|
2339
|
+
const zlib = require('zlib');
|
|
2340
|
+
return new Promise((resolve, reject) => {
|
|
2341
|
+
const lib = url.startsWith('https') ? https : http;
|
|
2342
|
+
lib.get(url, (res) => {
|
|
2343
|
+
if (res.statusCode !== 200) {
|
|
2344
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
2345
|
+
res.resume();
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
const rawChunks = [];
|
|
2349
|
+
res.on('data', (chunk) => rawChunks.push(chunk));
|
|
2350
|
+
res.on('end', () => {
|
|
2351
|
+
const raw = Buffer.concat(rawChunks);
|
|
2352
|
+
if (raw.length === 0) {
|
|
2353
|
+
resolve('(empty log)');
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
zlib.gunzip(raw, (err, result) => {
|
|
2357
|
+
if (err) {
|
|
2358
|
+
resolve(raw.toString('utf-8'));
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
resolve(result.toString('utf-8'));
|
|
2362
|
+
});
|
|
2363
|
+
});
|
|
2364
|
+
}).on('error', reject);
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
async function runWorkflowLog(executionId, orgOverride, outputFile, toConsole, useJson) {
|
|
2269
2368
|
if (!executionId) {
|
|
2270
2369
|
console.error(chalk_1.default.red('Error: Execution ID required'));
|
|
2271
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow
|
|
2370
|
+
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console] [--json]`));
|
|
2272
2371
|
process.exit(2);
|
|
2273
2372
|
}
|
|
2274
2373
|
const session = resolveSession();
|
|
2275
|
-
const domain = session
|
|
2276
|
-
const token = session.access_token;
|
|
2374
|
+
const { domain, access_token: token } = session;
|
|
2277
2375
|
const orgId = await resolveOrgId(domain, token, orgOverride);
|
|
2278
|
-
// Fetch execution details
|
|
2279
2376
|
const data = await graphqlRequest(domain, token, `
|
|
2280
2377
|
query ($organizationId: Int!, $executionId: UUID!) {
|
|
2281
2378
|
workflowExecution(organizationId: $organizationId, executionId: $executionId) {
|
|
2282
2379
|
executionId workflowId executionStatus executedAt durationMs
|
|
2380
|
+
txtLogUrl jsonLogUrl
|
|
2381
|
+
user { fullName email }
|
|
2283
2382
|
}
|
|
2284
2383
|
}
|
|
2285
2384
|
`, { organizationId: orgId, executionId });
|
|
@@ -2288,49 +2387,60 @@ async function runWorkflowLogs(executionId, orgOverride) {
|
|
|
2288
2387
|
console.error(chalk_1.default.red(`Execution not found: ${executionId}`));
|
|
2289
2388
|
process.exit(2);
|
|
2290
2389
|
}
|
|
2390
|
+
const logUrl = useJson ? ex.jsonLogUrl : ex.txtLogUrl;
|
|
2391
|
+
const logType = useJson ? 'json' : 'txt';
|
|
2392
|
+
const ext = useJson ? '.json' : '.log';
|
|
2393
|
+
if (!logUrl) {
|
|
2394
|
+
console.error(chalk_1.default.yellow(`No ${logType} log available for this execution.`));
|
|
2395
|
+
process.exit(0);
|
|
2396
|
+
}
|
|
2291
2397
|
const date = new Date(ex.executedAt).toLocaleString();
|
|
2292
2398
|
const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
|
|
2293
2399
|
const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
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}`;
|
|
2400
|
+
const userName = ex.user?.fullName || ex.user?.email || '';
|
|
2401
|
+
// Download log
|
|
2402
|
+
let logText;
|
|
2302
2403
|
try {
|
|
2303
|
-
|
|
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
|
-
}
|
|
2404
|
+
logText = await fetchGzipText(logUrl);
|
|
2329
2405
|
}
|
|
2330
|
-
catch {
|
|
2331
|
-
console.
|
|
2332
|
-
|
|
2406
|
+
catch (e) {
|
|
2407
|
+
console.error(chalk_1.default.red(`Failed to download log: ${e.message}`));
|
|
2408
|
+
process.exit(2);
|
|
2409
|
+
}
|
|
2410
|
+
// Pretty-print JSON if it's valid JSON
|
|
2411
|
+
if (useJson) {
|
|
2412
|
+
try {
|
|
2413
|
+
const parsed = JSON.parse(logText);
|
|
2414
|
+
logText = JSON.stringify(parsed, null, 2);
|
|
2415
|
+
}
|
|
2416
|
+
catch { /* keep as-is */ }
|
|
2417
|
+
}
|
|
2418
|
+
if (toConsole) {
|
|
2419
|
+
console.log(chalk_1.default.bold.cyan('\n Workflow Execution\n'));
|
|
2420
|
+
console.log(chalk_1.default.white(` ID: ${ex.executionId}`));
|
|
2421
|
+
console.log(chalk_1.default.white(` Workflow: ${ex.workflowId}`));
|
|
2422
|
+
console.log(chalk_1.default.white(` Status: ${statusColor(ex.executionStatus)}`));
|
|
2423
|
+
console.log(chalk_1.default.white(` Executed: ${date}`));
|
|
2424
|
+
console.log(chalk_1.default.white(` Duration: ${duration}`));
|
|
2425
|
+
if (userName)
|
|
2426
|
+
console.log(chalk_1.default.white(` User: ${userName}`));
|
|
2427
|
+
console.log(chalk_1.default.gray(`\n --- ${logType.toUpperCase()} Log ---\n`));
|
|
2428
|
+
console.log(logText);
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
// Save to file
|
|
2432
|
+
let filePath;
|
|
2433
|
+
if (outputFile) {
|
|
2434
|
+
filePath = path.resolve(outputFile);
|
|
2435
|
+
}
|
|
2436
|
+
else {
|
|
2437
|
+
const tmpDir = os.tmpdir();
|
|
2438
|
+
const dateStr = new Date(ex.executedAt).toISOString().slice(0, 10);
|
|
2439
|
+
filePath = path.join(tmpDir, `workflow-${ex.workflowId}-${dateStr}-${executionId}${ext}`);
|
|
2333
2440
|
}
|
|
2441
|
+
fs.writeFileSync(filePath, logText, 'utf-8');
|
|
2442
|
+
console.log(chalk_1.default.green(` ✓ ${logType.toUpperCase()} log saved: ${filePath}`));
|
|
2443
|
+
console.log(chalk_1.default.gray(` Execution: ${executionId} ${statusColor(ex.executionStatus)} ${date} ${duration}`));
|
|
2334
2444
|
}
|
|
2335
2445
|
// ============================================================================
|
|
2336
2446
|
// Publish Command
|
|
@@ -2942,6 +3052,21 @@ function parseArgs(args) {
|
|
|
2942
3052
|
}
|
|
2943
3053
|
options.orgId = parsed;
|
|
2944
3054
|
}
|
|
3055
|
+
else if (arg === '--vars') {
|
|
3056
|
+
options.vars = args[++i];
|
|
3057
|
+
}
|
|
3058
|
+
else if (arg === '--from') {
|
|
3059
|
+
options.from = args[++i];
|
|
3060
|
+
}
|
|
3061
|
+
else if (arg === '--to') {
|
|
3062
|
+
options.to = args[++i];
|
|
3063
|
+
}
|
|
3064
|
+
else if (arg === '--output' || arg === '-o') {
|
|
3065
|
+
options.output = args[++i];
|
|
3066
|
+
}
|
|
3067
|
+
else if (arg === '--console') {
|
|
3068
|
+
options.console = true;
|
|
3069
|
+
}
|
|
2945
3070
|
else if (!arg.startsWith('-')) {
|
|
2946
3071
|
files.push(arg);
|
|
2947
3072
|
}
|
|
@@ -3822,15 +3947,18 @@ async function main() {
|
|
|
3822
3947
|
else if (sub === 'delete') {
|
|
3823
3948
|
await runWorkflowDelete(files[1], options.orgId);
|
|
3824
3949
|
}
|
|
3825
|
-
else if (sub === '
|
|
3826
|
-
await
|
|
3950
|
+
else if (sub === 'execute') {
|
|
3951
|
+
await runWorkflowExecute(files[1], options.orgId, options.vars);
|
|
3827
3952
|
}
|
|
3828
3953
|
else if (sub === 'logs') {
|
|
3829
|
-
await runWorkflowLogs(files[1], options.orgId);
|
|
3954
|
+
await runWorkflowLogs(files[1], options.orgId, options.from, options.to);
|
|
3955
|
+
}
|
|
3956
|
+
else if (sub === 'log') {
|
|
3957
|
+
await runWorkflowLog(files[1], options.orgId, options.output, options.console, options.format === 'json');
|
|
3830
3958
|
}
|
|
3831
3959
|
else {
|
|
3832
3960
|
console.error(chalk_1.default.red(`Unknown workflow subcommand: ${sub || '(none)'}`));
|
|
3833
|
-
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <push|delete|
|
|
3961
|
+
console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <push|delete|execute|logs|log> ...`));
|
|
3834
3962
|
process.exit(2);
|
|
3835
3963
|
}
|
|
3836
3964
|
process.exit(0);
|