@cxtms/cx-schema 1.6.1 → 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/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, execute, logs)
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
@@ -141,6 +141,11 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
141
141
  ${chalk_1.default.green('--copy')} Copy component instead of moving (source unchanged, target gets higher priority)
142
142
  ${chalk_1.default.green('--org <id>')} Organization ID for server commands
143
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
144
149
 
145
150
  ${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
146
151
  ${chalk_1.default.gray('# Validate a module YAML file')}
@@ -210,10 +215,7 @@ ${chalk_1.default.bold.yellow('AUTH COMMANDS:')}
210
215
  ${chalk_1.default.gray('# Login to a CX environment')}
211
216
  ${chalk_1.default.cyan(`${PROGRAM_NAME} login https://qa.storevista.acuitive.net`)}
212
217
 
213
- ${chalk_1.default.gray('# Logout from a CX environment')}
214
- ${chalk_1.default.cyan(`${PROGRAM_NAME} logout https://qa.storevista.acuitive.net`)}
215
-
216
- ${chalk_1.default.gray('# List stored sessions')}
218
+ ${chalk_1.default.gray('# Logout from current session')}
217
219
  ${chalk_1.default.cyan(`${PROGRAM_NAME} logout`)}
218
220
 
219
221
  ${chalk_1.default.bold.yellow('PAT COMMANDS:')}
@@ -263,11 +265,16 @@ ${chalk_1.default.bold.yellow('WORKFLOW COMMANDS:')}
263
265
  ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId|file.yaml>`)}
264
266
  ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow execute <workflowId> --vars '{"city":"London"}'`)}
265
267
 
266
- ${chalk_1.default.gray('# List recent executions for a workflow')}
267
- ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow executions <workflowId|file.yaml>`)}
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`)}
268
271
 
269
- ${chalk_1.default.gray('# Show execution details and download logs')}
270
- ${chalk_1.default.cyan(`${PROGRAM_NAME} workflow logs <executionId>`)}
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')}
271
278
 
272
279
  ${chalk_1.default.bold.yellow('PUBLISH COMMANDS:')}
273
280
  ${chalk_1.default.gray('# Publish all modules and workflows from current project')}
@@ -1458,28 +1465,33 @@ function runSetupClaude() {
1458
1465
  // ============================================================================
1459
1466
  const AUTH_CALLBACK_PORT = 9000;
1460
1467
  const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
1461
- function getTokenDir() {
1462
- return path.join(os.homedir(), '.cxtms');
1468
+ function getSessionDir() {
1469
+ const projectName = path.basename(process.cwd());
1470
+ return path.join(os.homedir(), '.cxtms', projectName);
1463
1471
  }
1464
- function getTokenFilePath(domain) {
1465
- const hostname = new URL(domain).hostname;
1466
- return path.join(getTokenDir(), `${hostname}.json`);
1472
+ function getSessionFilePath() {
1473
+ return path.join(getSessionDir(), '.session.json');
1467
1474
  }
1468
- function readTokenFile(domain) {
1469
- const filePath = getTokenFilePath(domain);
1475
+ function readSessionFile() {
1476
+ const filePath = getSessionFilePath();
1470
1477
  if (!fs.existsSync(filePath))
1471
1478
  return null;
1472
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1479
+ try {
1480
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1481
+ }
1482
+ catch {
1483
+ return null;
1484
+ }
1473
1485
  }
1474
- function writeTokenFile(data) {
1475
- const dir = getTokenDir();
1486
+ function writeSessionFile(data) {
1487
+ const dir = getSessionDir();
1476
1488
  if (!fs.existsSync(dir)) {
1477
1489
  fs.mkdirSync(dir, { recursive: true });
1478
1490
  }
1479
- fs.writeFileSync(getTokenFilePath(data.domain), JSON.stringify(data, null, 2), 'utf-8');
1491
+ fs.writeFileSync(getSessionFilePath(), JSON.stringify(data, null, 2), 'utf-8');
1480
1492
  }
1481
- function deleteTokenFile(domain) {
1482
- const filePath = getTokenFilePath(domain);
1493
+ function deleteSessionFile() {
1494
+ const filePath = getSessionFilePath();
1483
1495
  if (fs.existsSync(filePath)) {
1484
1496
  fs.unlinkSync(filePath);
1485
1497
  }
@@ -1619,7 +1631,7 @@ async function refreshTokens(stored) {
1619
1631
  refresh_token: data.refresh_token || stored.refresh_token,
1620
1632
  expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
1621
1633
  };
1622
- writeTokenFile(updated);
1634
+ writeSessionFile(updated);
1623
1635
  return updated;
1624
1636
  }
1625
1637
  async function runLogin(domain) {
@@ -1663,51 +1675,30 @@ async function runLogin(domain) {
1663
1675
  // Step 6: Exchange code for tokens
1664
1676
  console.log(chalk_1.default.gray(' Exchanging authorization code...'));
1665
1677
  const tokens = await exchangeCodeForTokens(domain, clientId, code, codeVerifier);
1666
- // Step 7: Store tokens
1667
- writeTokenFile(tokens);
1678
+ // Step 7: Store session locally
1679
+ writeSessionFile(tokens);
1668
1680
  close();
1669
1681
  console.log(chalk_1.default.green(` ✓ Logged in to ${new URL(domain).hostname}`));
1670
- console.log(chalk_1.default.gray(` Token stored at: ${getTokenFilePath(domain)}\n`));
1671
- }
1672
- async function runLogout(domain) {
1673
- if (!domain) {
1674
- // List stored sessions
1675
- const dir = getTokenDir();
1676
- if (!fs.existsSync(dir)) {
1677
- console.log(chalk_1.default.gray('\n No stored sessions.\n'));
1678
- return;
1679
- }
1680
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
1681
- if (files.length === 0) {
1682
- console.log(chalk_1.default.gray('\n No stored sessions.\n'));
1683
- return;
1684
- }
1685
- console.log(chalk_1.default.bold.cyan('\n Stored sessions:\n'));
1686
- for (const f of files) {
1687
- const hostname = f.replace('.json', '');
1688
- console.log(chalk_1.default.white(` ${hostname}`));
1689
- }
1690
- console.log(chalk_1.default.gray(`\n Usage: ${PROGRAM_NAME} logout <url>\n`));
1691
- return;
1692
- }
1693
- // Normalize URL
1694
- if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
1695
- domain = `https://${domain}`;
1696
- }
1697
- domain = domain.replace(/\/+$/, '');
1698
- const stored = readTokenFile(domain);
1699
- if (!stored) {
1700
- console.log(chalk_1.default.gray(`\n No session found for ${new URL(domain).hostname}\n`));
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`));
1701
1689
  return;
1702
1690
  }
1703
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}`));
1704
1693
  // Revoke tokens (non-fatal)
1705
- console.log(chalk_1.default.gray(' Revoking tokens...'));
1706
- await revokeToken(domain, stored.client_id, stored.access_token);
1707
- await revokeToken(domain, stored.client_id, stored.refresh_token);
1708
- // Delete local file
1709
- deleteTokenFile(domain);
1710
- console.log(chalk_1.default.green(` ✓ Logged out from ${new URL(domain).hostname}\n`));
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`));
1711
1702
  }
1712
1703
  // ============================================================================
1713
1704
  // AppModule Commands
@@ -1720,7 +1711,7 @@ async function graphqlRequest(domain, token, query, variables) {
1720
1711
  if (process.env.CXTMS_AUTH)
1721
1712
  throw new Error('PAT token unauthorized (401). Check your CXTMS_AUTH token.');
1722
1713
  // Try refresh for OAuth sessions
1723
- const stored = readTokenFile(domain);
1714
+ const stored = readSessionFile();
1724
1715
  if (!stored)
1725
1716
  throw new Error('Session expired. Run `cx-cli login <url>` again.');
1726
1717
  try {
@@ -1812,43 +1803,20 @@ function resolveSession() {
1812
1803
  expires_at: 0,
1813
1804
  };
1814
1805
  }
1815
- // 1. Check app.yaml in CWD for server field
1816
- const appDomain = resolveDomainFromAppYaml();
1817
- if (appDomain) {
1818
- const stored = readTokenFile(appDomain);
1819
- if (stored)
1820
- return stored;
1821
- console.error(chalk_1.default.red(`Not logged in to ${appDomain}. Run \`cx-cli login ${appDomain}\` first.`));
1822
- process.exit(2);
1823
- }
1824
- // 2. Check for single session
1825
- const dir = getTokenDir();
1826
- if (!fs.existsSync(dir)) {
1827
- console.error(chalk_1.default.red('Not logged in. Run `cx-cli login <url>` first.'));
1828
- process.exit(2);
1829
- }
1830
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
1831
- if (files.length === 0) {
1832
- console.error(chalk_1.default.red('Not logged in. Run `cx-cli login <url>` first.'));
1833
- process.exit(2);
1834
- }
1835
- if (files.length === 1) {
1836
- return JSON.parse(fs.readFileSync(path.join(dir, files[0]), 'utf-8'));
1837
- }
1838
- // 3. Multiple sessions — error
1839
- console.error(chalk_1.default.red('Multiple sessions found:'));
1840
- for (const f of files) {
1841
- console.error(chalk_1.default.white(` ${f.replace('.json', '')}`));
1842
- }
1843
- console.error(chalk_1.default.gray('Add `server` field to app.yaml or use a single login session.'));
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.'));
1844
1812
  process.exit(2);
1845
1813
  }
1846
1814
  async function resolveOrgId(domain, token, override) {
1847
1815
  // 1. Explicit override
1848
1816
  if (override !== undefined)
1849
1817
  return override;
1850
- // 2. Cached in token file
1851
- const stored = readTokenFile(domain);
1818
+ // 2. Cached in session file
1819
+ const stored = readSessionFile();
1852
1820
  if (stored?.organization_id)
1853
1821
  return stored.organization_id;
1854
1822
  // 3. Query server
@@ -1864,7 +1832,7 @@ async function resolveOrgId(domain, token, override) {
1864
1832
  // Cache it
1865
1833
  if (stored) {
1866
1834
  stored.organization_id = orgId;
1867
- writeTokenFile(stored);
1835
+ writeSessionFile(stored);
1868
1836
  }
1869
1837
  return orgId;
1870
1838
  }
@@ -2072,9 +2040,9 @@ async function runOrgsUse(orgIdStr) {
2072
2040
  console.error('');
2073
2041
  process.exit(2);
2074
2042
  }
2075
- // Save to token file
2043
+ // Save to session file
2076
2044
  session.organization_id = orgId;
2077
- writeTokenFile(session);
2045
+ writeSessionFile(session);
2078
2046
  console.log(chalk_1.default.green(`\n ✓ Context set to: ${match.companyName} (${orgId})\n`));
2079
2047
  }
2080
2048
  async function runOrgsSelect() {
@@ -2113,7 +2081,7 @@ async function runOrgsSelect() {
2113
2081
  }
2114
2082
  const selected = orgs[idx];
2115
2083
  session.organization_id = selected.organizationId;
2116
- writeTokenFile(session);
2084
+ writeSessionFile(session);
2117
2085
  console.log(chalk_1.default.green(`\n ✓ Context set to: ${selected.companyName} (${selected.organizationId})\n`));
2118
2086
  }
2119
2087
  // ============================================================================
@@ -2287,57 +2255,85 @@ async function runWorkflowExecute(workflowIdOrFile, orgOverride, variables) {
2287
2255
  }
2288
2256
  console.log('');
2289
2257
  }
2290
- async function runWorkflowExecutions(workflowIdOrFile, orgOverride) {
2291
- if (!workflowIdOrFile) {
2292
- console.error(chalk_1.default.red('Error: Workflow ID or YAML file required'));
2293
- console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow executions <workflowId|file.yaml> [--org <id>]`));
2294
- process.exit(2);
2295
- }
2296
- const session = resolveSession();
2297
- const domain = session.domain;
2298
- const token = session.access_token;
2299
- const orgId = await resolveOrgId(domain, token, orgOverride);
2300
- // Resolve workflowId — accept UUID directly or extract from YAML file
2301
- let workflowId = workflowIdOrFile;
2258
+ function resolveWorkflowId(workflowIdOrFile) {
2302
2259
  if (workflowIdOrFile.endsWith('.yaml') || workflowIdOrFile.endsWith('.yml')) {
2303
2260
  if (!fs.existsSync(workflowIdOrFile)) {
2304
2261
  console.error(chalk_1.default.red(`Error: File not found: ${workflowIdOrFile}`));
2305
2262
  process.exit(2);
2306
2263
  }
2307
2264
  const parsed = yaml_1.default.parse(fs.readFileSync(workflowIdOrFile, 'utf-8'));
2308
- workflowId = parsed?.workflow?.workflowId;
2309
- if (!workflowId) {
2265
+ const id = parsed?.workflow?.workflowId;
2266
+ if (!id) {
2310
2267
  console.error(chalk_1.default.red('Error: Workflow YAML is missing workflow.workflowId'));
2311
2268
  process.exit(2);
2312
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);
2313
2294
  }
2314
2295
  const data = await graphqlRequest(domain, token, `
2315
2296
  query ($organizationId: Int!, $workflowId: UUID!) {
2316
- workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 25) {
2297
+ workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 100) {
2317
2298
  totalCount
2318
- items { executionId executionStatus executedAt durationMs userId }
2299
+ items { executionId executionStatus executedAt durationMs txtLogUrl user { fullName email } }
2319
2300
  }
2320
2301
  }
2321
2302
  `, { organizationId: orgId, workflowId });
2322
- const items = data?.workflowExecutions?.items || [];
2303
+ let items = data?.workflowExecutions?.items || [];
2323
2304
  const total = data?.workflowExecutions?.totalCount || 0;
2324
- console.log(chalk_1.default.bold.cyan('\n Workflow Executions\n'));
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'));
2325
2315
  console.log(chalk_1.default.gray(` Server: ${new URL(domain).hostname}`));
2326
2316
  console.log(chalk_1.default.gray(` Workflow: ${workflowId}`));
2327
- console.log(chalk_1.default.gray(` Total: ${total}\n`));
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`));
2328
2322
  if (items.length === 0) {
2329
2323
  console.log(chalk_1.default.gray(' No executions found.\n'));
2330
2324
  return;
2331
2325
  }
2332
- // Sort descending by executedAt (API may not support ordering)
2333
- items.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime());
2334
2326
  for (const ex of items) {
2335
2327
  const date = new Date(ex.executedAt).toLocaleString();
2336
2328
  const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
2337
2329
  const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
2338
- console.log(chalk_1.default.white(` ${ex.executionId} ${statusColor(ex.executionStatus.padEnd(10))} ${date} ${chalk_1.default.gray(duration)}`));
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) : ''}`);
2339
2333
  }
2340
- 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`));
2341
2337
  }
2342
2338
  function fetchGzipText(url) {
2343
2339
  const zlib = require('zlib');
@@ -2361,23 +2357,22 @@ function fetchGzipText(url) {
2361
2357
  if (err) {
2362
2358
  resolve(raw.toString('utf-8'));
2363
2359
  return;
2364
- } // fallback: maybe not gzipped
2360
+ }
2365
2361
  resolve(result.toString('utf-8'));
2366
2362
  });
2367
2363
  });
2368
2364
  }).on('error', reject);
2369
2365
  });
2370
2366
  }
2371
- async function runWorkflowLogs(executionId, orgOverride) {
2367
+ async function runWorkflowLog(executionId, orgOverride, outputFile, toConsole, useJson) {
2372
2368
  if (!executionId) {
2373
2369
  console.error(chalk_1.default.red('Error: Execution ID required'));
2374
- console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow logs <executionId> [--org <id>]`));
2370
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow log <executionId> [--output <file>] [--console] [--json]`));
2375
2371
  process.exit(2);
2376
2372
  }
2377
2373
  const session = resolveSession();
2378
2374
  const { domain, access_token: token } = session;
2379
2375
  const orgId = await resolveOrgId(domain, token, orgOverride);
2380
- // Fetch execution details with log URLs
2381
2376
  const data = await graphqlRequest(domain, token, `
2382
2377
  query ($organizationId: Int!, $executionId: UUID!) {
2383
2378
  workflowExecution(organizationId: $organizationId, executionId: $executionId) {
@@ -2392,33 +2387,60 @@ async function runWorkflowLogs(executionId, orgOverride) {
2392
2387
  console.error(chalk_1.default.red(`Execution not found: ${executionId}`));
2393
2388
  process.exit(2);
2394
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
+ }
2395
2397
  const date = new Date(ex.executedAt).toLocaleString();
2396
2398
  const duration = ex.durationMs != null ? `${(ex.durationMs / 1000).toFixed(1)}s` : '?';
2397
2399
  const statusColor = ex.executionStatus === 'Success' ? chalk_1.default.green : ex.executionStatus === 'Failed' ? chalk_1.default.red : chalk_1.default.yellow;
2398
2400
  const userName = ex.user?.fullName || ex.user?.email || '';
2399
- console.log(chalk_1.default.bold.cyan('\n Workflow Execution\n'));
2400
- console.log(chalk_1.default.white(` ID: ${ex.executionId}`));
2401
- console.log(chalk_1.default.white(` Workflow: ${ex.workflowId}`));
2402
- console.log(chalk_1.default.white(` Status: ${statusColor(ex.executionStatus)}`));
2403
- console.log(chalk_1.default.white(` Executed: ${date}`));
2404
- console.log(chalk_1.default.white(` Duration: ${duration}`));
2405
- if (userName)
2406
- console.log(chalk_1.default.white(` User: ${userName}`));
2407
- // Download and display the text log
2408
- if (ex.txtLogUrl) {
2401
+ // Download log
2402
+ let logText;
2403
+ try {
2404
+ logText = await fetchGzipText(logUrl);
2405
+ }
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) {
2409
2412
  try {
2410
- const logText = await fetchGzipText(ex.txtLogUrl);
2411
- console.log(chalk_1.default.gray('\n --- Log ---\n'));
2412
- console.log(logText);
2413
- }
2414
- catch (e) {
2415
- console.log(chalk_1.default.yellow(`\n Failed to download log: ${e.message}`));
2416
- console.log(chalk_1.default.gray(` txtLogUrl: ${ex.txtLogUrl}\n`));
2417
- }
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);
2418
2435
  }
2419
2436
  else {
2420
- console.log(chalk_1.default.gray('\n No log available for this execution.\n'));
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}`);
2421
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}`));
2422
2444
  }
2423
2445
  // ============================================================================
2424
2446
  // Publish Command
@@ -3033,6 +3055,18 @@ function parseArgs(args) {
3033
3055
  else if (arg === '--vars') {
3034
3056
  options.vars = args[++i];
3035
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
+ }
3036
3070
  else if (!arg.startsWith('-')) {
3037
3071
  files.push(arg);
3038
3072
  }
@@ -3916,15 +3950,15 @@ async function main() {
3916
3950
  else if (sub === 'execute') {
3917
3951
  await runWorkflowExecute(files[1], options.orgId, options.vars);
3918
3952
  }
3919
- else if (sub === 'executions') {
3920
- await runWorkflowExecutions(files[1], options.orgId);
3921
- }
3922
3953
  else if (sub === 'logs') {
3923
- 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');
3924
3958
  }
3925
3959
  else {
3926
3960
  console.error(chalk_1.default.red(`Unknown workflow subcommand: ${sub || '(none)'}`));
3927
- console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <push|delete|execute|executions|logs> ...`));
3961
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} workflow <push|delete|execute|logs|log> ...`));
3928
3962
  process.exit(2);
3929
3963
  }
3930
3964
  process.exit(0);