@solidactions/cli 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -312,7 +312,7 @@ async function deploy(projectName, sourcePath, options = {}) {
312
312
  });
313
313
  // Dockerfile always at archive root, referencing tenantcode/
314
314
  const universalDockerfile = [
315
- 'FROM node:20-alpine',
315
+ 'FROM node:24-alpine',
316
316
  'WORKDIR /app',
317
317
  'COPY tenantcode/package.json tenantcode/package-lock.json* ./',
318
318
  'RUN npm install',
@@ -26,6 +26,8 @@ async function runs(projectName, options = {}) {
26
26
  params.workflow = options.workflow;
27
27
  if (options.detailed)
28
28
  params.detailed = '1';
29
+ if (options.hasErrors)
30
+ params.has_errors = '1';
29
31
  const response = await axios_1.default.get(`${config.host}/api/v1/runs`, {
30
32
  headers: (0, api_1.getApiHeaders)(config),
31
33
  params,
@@ -97,8 +99,10 @@ function displayDetailedList(runsList, projectName) {
97
99
  const status = run.execution_status || run.status || '?';
98
100
  const statusColor = getStatusColor(status);
99
101
  const exitStr = run.exit_code !== null && run.exit_code !== undefined ? ` (exit ${run.exit_code})` : '';
102
+ const isSilentFailure = hasRunErrors(run) && isSuccessStatus(status);
100
103
  console.log('');
101
- console.log(chalk_1.default.bold(` Run #${run.id}`) + chalk_1.default.gray(` — ${run.workflow_name || '?'} (${run.project_name || '?'})`));
104
+ const silentTag = isSilentFailure ? chalk_1.default.yellow(' [DEGRADED]') : '';
105
+ console.log(chalk_1.default.bold(` Run #${run.id}`) + chalk_1.default.gray(` — ${run.workflow_name || '?'} (${run.project_name || '?'})`) + silentTag);
102
106
  console.log(` Status: ${statusColor(status)}${chalk_1.default.gray(exitStr)}`);
103
107
  console.log(` Trigger: ${chalk_1.default.gray(run.triggered_by || '-')}`);
104
108
  // Timeline
@@ -108,18 +112,38 @@ function displayDetailedList(runsList, projectName) {
108
112
  console.log(` Started: ${chalk_1.default.gray(formatTs(run.timeline.started))}`);
109
113
  console.log(` Completed: ${chalk_1.default.gray(formatTs(run.timeline.completed))}`);
110
114
  }
111
- // Steps summary
115
+ // Steps detail
112
116
  if (run.steps && run.steps.length > 0) {
113
- const completed = run.steps.filter((s) => s.completed_at || s.completed_at_epoch_ms).length;
114
- const totalDuration = run.steps.reduce((sum, s) => sum + (s.duration_ms || 0), 0);
115
- console.log(` Steps: ${chalk_1.default.gray(`${completed}/${run.steps.length} completed (${formatDuration(totalDuration)} total)`)}`);
117
+ displayStepsDetail(run.steps);
118
+ }
119
+ // Workflow output
120
+ if (run.output) {
121
+ const outputStr = JSON.stringify(unwrapOutput(run.output), null, 2);
122
+ const lines = outputStr.split('\n');
123
+ if (lines.length === 1) {
124
+ console.log(` Output: ${chalk_1.default.gray(truncate(outputStr, 60))}`);
125
+ }
126
+ else {
127
+ console.log(` Output:`);
128
+ for (const line of lines.slice(0, 5)) {
129
+ console.log(chalk_1.default.gray(` ${line}`));
130
+ }
131
+ if (lines.length > 5) {
132
+ console.log(chalk_1.default.gray(` ... (${lines.length - 5} more lines)`));
133
+ }
134
+ }
135
+ }
136
+ // Workflow error
137
+ if (run.error) {
138
+ const errMsg = typeof run.error === 'string' ? run.error : JSON.stringify(run.error);
139
+ console.log(` ${chalk_1.default.bold.red('Error:')} ${chalk_1.default.red(truncate(errMsg, 60))}`);
116
140
  }
117
141
  // Logs snippet (first errors or last 3 lines)
118
142
  if (run.logs && typeof run.logs === 'string' && run.logs.trim()) {
119
143
  const lines = run.logs.trim().split('\n');
120
144
  const errorLines = lines.filter((l) => /error|fail|exception/i.test(l));
121
145
  if (errorLines.length > 0) {
122
- console.log(` Errors:`);
146
+ console.log(` Logs:`);
123
147
  for (const line of errorLines.slice(0, 3)) {
124
148
  console.log(chalk_1.default.red(` ${truncate(line, 80)}`));
125
149
  }
@@ -129,7 +153,91 @@ function displayDetailedList(runsList, projectName) {
129
153
  console.log('');
130
154
  console.log(chalk_1.default.gray(`Showing ${runsList.length} run(s)`));
131
155
  }
156
+ function displayStepsDetail(steps) {
157
+ const completed = steps.filter((s) => s.completed_at || s.completed_at_epoch_ms).length;
158
+ const totalDuration = steps.reduce((sum, s) => sum + (s.duration_ms || 0), 0);
159
+ const errorCount = steps.filter((s) => s.error || s.status === 'error' || s.status === 'failed').length;
160
+ const retryCount = steps.filter((s) => s.retried || s.status === 'retried').length;
161
+ let summary = `${completed}/${steps.length} completed (${formatDuration(totalDuration)} total)`;
162
+ if (errorCount > 0)
163
+ summary += chalk_1.default.red(` · ${errorCount} errored`);
164
+ if (retryCount > 0)
165
+ summary += chalk_1.default.yellow(` · ${retryCount} retried`);
166
+ console.log(` Steps: ${chalk_1.default.gray(summary)}`);
167
+ // Per-step detail table
168
+ console.log(chalk_1.default.gray(` ${'NAME'.padEnd(24)}${'STATUS'.padEnd(12)}${'DURATION'.padEnd(10)}OUTPUT`));
169
+ console.log(chalk_1.default.gray(` ${'─'.repeat(70)}`));
170
+ for (const step of steps) {
171
+ const name = truncate(step.name || '?', 23).padEnd(24);
172
+ const stepStatus = getStepStatus(step);
173
+ const stepStatusColor = getStepStatusColor(stepStatus);
174
+ const duration = formatDuration(step.duration_ms);
175
+ const output = step.output ? truncate(JSON.stringify(unwrapOutput(step.output)), 40) : '-';
176
+ console.log(` ${name}${stepStatusColor(stepStatus.padEnd(12))}${chalk_1.default.gray(duration.padEnd(10))}${chalk_1.default.gray(output)}`);
177
+ if (step.error) {
178
+ const errMsg = typeof step.error === 'string' ? step.error : JSON.stringify(step.error);
179
+ console.log(chalk_1.default.red(` error: ${truncate(errMsg, 70)}`));
180
+ }
181
+ }
182
+ }
132
183
  // ─── Utility ───────────────────────────────────────────────────────────────
184
+ function unwrapOutput(output) {
185
+ if (output && output.__solidactions_serializer === 'superjson' && output.json) {
186
+ return output.json;
187
+ }
188
+ return output;
189
+ }
190
+ /**
191
+ * Determine if a run has any errors (step errors, retries, or non-empty error arrays in output).
192
+ */
193
+ function hasRunErrors(run) {
194
+ // Check step-level errors
195
+ if (run.steps && run.steps.some((s) => s.error || s.status === 'error' || s.status === 'failed' || s.retried || s.status === 'retried')) {
196
+ return true;
197
+ }
198
+ // Check workflow-level error
199
+ if (run.error)
200
+ return true;
201
+ // Check output.errors array
202
+ const output = unwrapOutput(run.output);
203
+ if (output && Array.isArray(output.errors) && output.errors.length > 0)
204
+ return true;
205
+ return false;
206
+ }
207
+ function isSuccessStatus(status) {
208
+ const s = status?.toLowerCase();
209
+ return s === 'completed' || s === 'success';
210
+ }
211
+ function getStepStatus(step) {
212
+ if (step.status)
213
+ return step.status;
214
+ if (step.error)
215
+ return 'error';
216
+ if (step.completed_at || step.completed_at_epoch_ms)
217
+ return 'completed';
218
+ if (step.started_at || step.started_at_epoch_ms)
219
+ return 'running';
220
+ return 'pending';
221
+ }
222
+ function getStepStatusColor(status) {
223
+ switch (status?.toLowerCase()) {
224
+ case 'completed':
225
+ case 'success':
226
+ return chalk_1.default.green;
227
+ case 'running':
228
+ return chalk_1.default.blue;
229
+ case 'pending':
230
+ case 'queued':
231
+ return chalk_1.default.yellow;
232
+ case 'failed':
233
+ case 'error':
234
+ return chalk_1.default.red;
235
+ case 'retried':
236
+ return chalk_1.default.yellow;
237
+ default:
238
+ return chalk_1.default.gray;
239
+ }
240
+ }
133
241
  /**
134
242
  * Parse --since value into epoch ms.
135
243
  * Accepts: "1h", "30m", "2d", "1w", or ISO date string.
@@ -16,16 +16,18 @@ async function runView(runId, options) {
16
16
  });
17
17
  const runData = runResponse.data;
18
18
  // Build timeline
19
+ // Duration = container start → container stop (not triggered → stop)
19
20
  const triggeredAt = runData.triggered_at || runData.created_at;
20
21
  const sessionStartedMs = runData.session_started_at_epoch_ms;
21
22
  const sessionCompletedMs = runData.session_completed_at_epoch_ms;
22
- const triggeredMs = triggeredAt ? new Date(triggeredAt).getTime() : null;
23
- const totalMs = (triggeredMs && sessionCompletedMs) ? sessionCompletedMs - triggeredMs : null;
23
+ const durationMs = (sessionStartedMs && sessionCompletedMs)
24
+ ? sessionCompletedMs - sessionStartedMs
25
+ : null;
24
26
  const timeline = {
25
27
  triggered: triggeredAt || null,
26
28
  started: sessionStartedMs ? new Date(sessionStartedMs).toISOString() : null,
27
29
  completed: sessionCompletedMs ? new Date(sessionCompletedMs).toISOString() : null,
28
- totalMs,
30
+ durationMs,
29
31
  };
30
32
  // --logs: fetch and display raw logs
31
33
  if (options.logs) {
@@ -127,7 +129,7 @@ function displayFullView(runData, timeline, steps, logsData) {
127
129
  console.log(` Status: ${statusColor(status)}${chalk_1.default.gray(exitStr)}`);
128
130
  console.log(` Trigger: ${chalk_1.default.gray(runData.triggered_by || '-')}`);
129
131
  // Timeline
130
- displayTimeline(runData, timeline);
132
+ displayTimeline(runData, timeline, steps);
131
133
  // Steps
132
134
  if (steps.length > 0) {
133
135
  console.log('');
@@ -170,17 +172,27 @@ function displayFullView(runData, timeline, steps, logsData) {
170
172
  }
171
173
  console.log('');
172
174
  }
173
- function displayTimeline(runData, timeline) {
175
+ function displayTimeline(runData, timeline, steps = []) {
174
176
  const formatTs = (iso) => {
175
177
  if (!iso)
176
178
  return '-';
177
179
  const d = new Date(iso);
178
180
  return d.toLocaleString();
179
181
  };
180
- console.log(` Triggered: ${chalk_1.default.gray(formatTs(timeline.triggered))}`);
182
+ // Compute startup: triggered → first step start
183
+ let startupMs = null;
184
+ if (timeline.triggered && steps.length > 0 && steps[0].startedAt) {
185
+ const triggeredMs = new Date(timeline.triggered).getTime();
186
+ const firstStepMs = new Date(steps[0].startedAt).getTime();
187
+ startupMs = firstStepMs - triggeredMs;
188
+ }
189
+ console.log(` Created: ${chalk_1.default.gray(formatTs(timeline.triggered))}`);
181
190
  console.log(` Started: ${chalk_1.default.gray(formatTs(timeline.started))}`);
182
191
  console.log(` Completed: ${chalk_1.default.gray(formatTs(timeline.completed))}`);
183
- console.log(` Duration: ${chalk_1.default.gray(timeline.totalMs ? formatDuration(timeline.totalMs) : '-')}`);
192
+ if (startupMs !== null && startupMs > 0) {
193
+ console.log(` Startup: ${chalk_1.default.gray(formatDuration(startupMs))}`);
194
+ }
195
+ console.log(` Duration: ${chalk_1.default.gray(timeline.durationMs ? formatDuration(timeline.durationMs) : '-')}`);
184
196
  }
185
197
  function displayStepsTable(steps, indent = ' ') {
186
198
  if (steps.length === 0) {
package/dist/index.js CHANGED
@@ -126,6 +126,7 @@ runCmd
126
126
  .option('--since <duration>', 'Filter to runs since (e.g., 1h, 30m, 2d, 1w)')
127
127
  .option('--workflow <name>', 'Filter by workflow name')
128
128
  .option('--detailed', 'Include timeline, steps, and logs per run (default limit: 5)')
129
+ .option('--has-errors', 'Show only runs with errors (step errors, retries, or degraded results)')
129
130
  .option('--json', 'Output as JSON')
130
131
  .action((projectName, options) => {
131
132
  (0, run_list_1.runs)(projectName, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {