@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/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, executions, 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
@@ -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 appmodule commands
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 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')}
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('# 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
+ ${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('# Show execution details and logs')}
266
- ${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')}
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 getTokenDir() {
1458
- return path.join(os.homedir(), '.cxtms');
1468
+ function getSessionDir() {
1469
+ const projectName = path.basename(process.cwd());
1470
+ return path.join(os.homedir(), '.cxtms', projectName);
1459
1471
  }
1460
- function getTokenFilePath(domain) {
1461
- const hostname = new URL(domain).hostname;
1462
- return path.join(getTokenDir(), `${hostname}.json`);
1472
+ function getSessionFilePath() {
1473
+ return path.join(getSessionDir(), '.session.json');
1463
1474
  }
1464
- function readTokenFile(domain) {
1465
- const filePath = getTokenFilePath(domain);
1475
+ function readSessionFile() {
1476
+ const filePath = getSessionFilePath();
1466
1477
  if (!fs.existsSync(filePath))
1467
1478
  return null;
1468
- 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
+ }
1469
1485
  }
1470
- function writeTokenFile(data) {
1471
- const dir = getTokenDir();
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(getTokenFilePath(data.domain), JSON.stringify(data, null, 2), 'utf-8');
1491
+ fs.writeFileSync(getSessionFilePath(), JSON.stringify(data, null, 2), 'utf-8');
1476
1492
  }
1477
- function deleteTokenFile(domain) {
1478
- const filePath = getTokenFilePath(domain);
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
- writeTokenFile(updated);
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 tokens
1663
- writeTokenFile(tokens);
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(` 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`));
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
- 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`));
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 = readTokenFile(domain);
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 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.'));
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 token file
1847
- const stored = readTokenFile(domain);
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
- writeTokenFile(stored);
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 token file
2043
+ // Save to session file
2072
2044
  session.organization_id = orgId;
2073
- writeTokenFile(session);
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
- writeTokenFile(session);
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 runWorkflowExecutions(workflowIdOrFile, orgOverride) {
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 executions <workflowId|file.yaml> [--org <id>]`));
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.domain;
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 — accept UUID directly or extract from YAML file
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: 25) {
2297
+ workflowExecutions(organizationId: $organizationId, workflowId: $workflowId, take: 100) {
2243
2298
  totalCount
2244
- items { executionId executionStatus executedAt durationMs userId }
2299
+ items { executionId executionStatus executedAt durationMs txtLogUrl user { fullName email } }
2245
2300
  }
2246
2301
  }
2247
2302
  `, { organizationId: orgId, workflowId });
2248
- const items = data?.workflowExecutions?.items || [];
2303
+ let items = data?.workflowExecutions?.items || [];
2249
2304
  const total = data?.workflowExecutions?.totalCount || 0;
2250
- 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'));
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}\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`));
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
- 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) : ''}`);
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
- async function runWorkflowLogs(executionId, orgOverride) {
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 logs <executionId> [--org <id>]`));
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.domain;
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
- 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}`;
2400
+ const userName = ex.user?.fullName || ex.user?.email || '';
2401
+ // Download log
2402
+ let logText;
2302
2403
  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
- }
2404
+ logText = await fetchGzipText(logUrl);
2329
2405
  }
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`));
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 === 'executions') {
3826
- await runWorkflowExecutions(files[1], options.orgId);
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|executions|logs> ...`));
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);