@solidactions/cli 0.7.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,39 +11,82 @@ npm install -g @solidactions/cli
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- # Initialize with your API key
15
- solidactions init <api-key>
16
-
17
- # Deploy a project
18
- solidactions deploy <project-name> <path>
14
+ solidactions init <api-key> # Initialize (prompts for workspace)
15
+ solidactions init <api-key> --workspace <name> # Initialize with workspace
16
+ solidactions project deploy <project> <path> # Deploy a project
17
+ solidactions run start <project> <workflow> # Trigger a workflow
18
+ solidactions run view <run-id> # Inspect a run
19
19
  ```
20
20
 
21
21
  ## Commands
22
22
 
23
+ Use `solidactions <command> --help` for full flag details on any command.
24
+
25
+ ### Top-level
26
+
23
27
  | Command | Description |
24
28
  |---------|-------------|
25
- | `init <api-key>` | Initialize CLI with your API key |
29
+ | `init <api-key>` | Initialize CLI with API key |
26
30
  | `logout` | Remove saved credentials |
27
31
  | `whoami` | Show current configuration |
28
- | `deploy <project> [path]` | Deploy a project to SolidActions |
29
- | `pull <project> [path]` | Pull project source from SolidActions |
30
- | `run <project> <workflow>` | Trigger a workflow run |
31
- | `runs [project]` | List recent workflow runs |
32
- | `logs <run-id>` | View logs for a workflow run |
33
- | `logs:build <project>` | View build/deployment logs |
34
- | `env:set <key> <value>` | Set a global variable (create or update) |
35
- | `env:set <project> <key> <value>` | Set a project variable (create or update) |
36
- | `env:list [project]` | List environment variables |
37
- | `env:delete <key>` | Delete an environment variable |
38
- | `env:map <project> <key> <global-key>` | Map a global variable to a project |
39
- | `env:pull <project>` | Pull resolved env vars (including OAuth tokens) to a local file |
40
- | `env:push <project> [path]` | Push .env values to a project |
41
- | `schedule:set <project> <cron>` | Set a cron schedule for a workflow |
42
- | `schedule:list <project>` | List schedules for a project |
43
- | `schedule:delete <project> <id>` | Delete a schedule |
44
- | `webhooks <project>` | List webhook URLs for a project |
45
-
46
- See [docs/cli.md](docs/cli.md) for full documentation.
32
+ | `dev <file>` | Run a workflow locally (no deploy needed) |
33
+
34
+ ### project
35
+
36
+ | Command | Key Flags | Description |
37
+ |---------|-----------|-------------|
38
+ | `project deploy <name> [path]` | `-e`, `--create`, `--config-only` | Deploy or sync config only |
39
+ | `project pull <name> [path]` | `-y` | Pull source (warns before overwriting) |
40
+ | `project logs <name>` | | View build logs |
41
+ | `project list` | | List all projects |
42
+
43
+ ### run
44
+
45
+ | Command | Key Flags | Description |
46
+ |---------|-----------|-------------|
47
+ | `run start <project> <workflow>` | `-e`, `-i`, `-w` | Trigger a workflow run |
48
+ | `run list [project]` | `--json`, `--detailed`, `--status`, `--since`, `--workflow`, `--limit`, `--offset` | List and filter runs |
49
+ | `run view <run-id>` | `--json`, `--timeline`, `--steps`, `--logs` | Inspect a run |
50
+
51
+ ### env
52
+
53
+ | Command | Key Flags | Description |
54
+ |---------|-----------|-------------|
55
+ | `env set <key> <value>` | `-s`, `-y`, `--staging-value`, `--dev-value` | Set global variable (warns before overwriting) |
56
+ | `env set <project> <key> <value>` | `-e`, `-s`, `-y` | Set project variable (warns before overwriting) |
57
+ | `env list [project]` | `-e` | List variables |
58
+ | `env delete <key-or-project> [key]` | `-y` | Delete a variable |
59
+ | `env map <project> <key> <global-key>` | `-y` | Map global to project key (warns before overwriting) |
60
+ | `env pull <project>` | `-e`, `-o`, `-y`, `--update-oauth` | Pull resolved env vars to .env file |
61
+ | `env push <project> [path]` | `-e`, `-y`, `--new-only`, `--include-undeclared` | Push .env values to project |
62
+
63
+ ### schedule
64
+
65
+ | Command | Key Flags | Description |
66
+ |---------|-----------|-------------|
67
+ | `schedule set <project> <cron>` | `-w`, `-i`, `-y` | Set cron schedule (warns if exists) |
68
+ | `schedule list <project>` | | List schedules |
69
+ | `schedule delete <project> <id>` | `-y` | Delete a schedule |
70
+
71
+ ### webhook
72
+
73
+ | Command | Key Flags | Description |
74
+ |---------|-----------|-------------|
75
+ | `webhook list <project>` | `-e`, `--show-secrets` | List webhook URLs |
76
+
77
+ ### workspace
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `workspace list` | List all workspaces |
82
+ | `workspace set <id>` | Set active workspace (by ID, slug, or name) |
83
+
84
+ ### ai
85
+
86
+ | Command | Key Flags | Description |
87
+ |---------|-----------|-------------|
88
+ | `ai init` | `--claude`, `--agents` | Install AI helper docs |
89
+ | `ai examples [names...]` | `--all`, `--overwrite` | Install example workflows |
47
90
 
48
91
  ## Development
49
92
 
@@ -83,7 +83,7 @@ async function aiExamples(names, options = {}) {
83
83
  // Detect AI helper file and upsert examples reference
84
84
  const aiHelperFile = (0, markers_1.findAiHelperFile)();
85
85
  if (!aiHelperFile) {
86
- console.log(chalk_1.default.yellow('No AI helper file found. Run "solidactions ai:init" first to create one.'));
86
+ console.log(chalk_1.default.yellow('No AI helper file found. Run "solidactions ai init" first to create one.'));
87
87
  }
88
88
  else {
89
89
  // List ALL installed examples (existing + newly installed)
@@ -191,6 +191,16 @@ async function deploy(projectName, sourcePath, options = {}) {
191
191
  process.exit(1);
192
192
  }
193
193
  }
194
+ // --config-only: sync YAML env declarations without building
195
+ if (options.configOnly) {
196
+ if (!yamlConfig) {
197
+ console.error(chalk_1.default.red('Cannot sync config: solidactions.yaml not found or invalid.'));
198
+ process.exit(1);
199
+ }
200
+ await pushYamlDeclarations(config, projectSlug, yamlConfig);
201
+ console.log(chalk_1.default.green(`✓ Config synced for ${projectSlug}${envLabel}`));
202
+ return;
203
+ }
194
204
  const archivePath = path_1.default.join(sourceDir, '.steps-deploy.tar.gz');
195
205
  const output = fs_1.default.createWriteStream(archivePath);
196
206
  const archive = (0, archiver_1.default)('tar', { gzip: true, gzipOptions: { level: 9 } });
@@ -302,7 +312,7 @@ async function deploy(projectName, sourcePath, options = {}) {
302
312
  });
303
313
  // Dockerfile always at archive root, referencing tenantcode/
304
314
  const universalDockerfile = [
305
- 'FROM node:20-alpine',
315
+ 'FROM node:24-alpine',
306
316
  'WORKDIR /app',
307
317
  'COPY tenantcode/package.json tenantcode/package-lock.json* ./',
308
318
  'RUN npm install',
@@ -6,8 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.envMap = envMap;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = __importDefault(require("prompts"));
9
10
  const api_1 = require("../utils/api");
10
- async function envMap(projectName, projectKey, globalKey) {
11
+ async function envMap(projectName, projectKey, globalKey, options = {}) {
11
12
  const config = await (0, api_1.requireConfigWithWorkspace)();
12
13
  console.log(chalk_1.default.blue(`Mapping global variable "${globalKey}" to project key "${projectKey}" in "${projectName}"...`));
13
14
  try {
@@ -19,9 +20,31 @@ async function envMap(projectName, projectKey, globalKey) {
19
20
  const globalVar = variables.find((v) => v.key === globalKey);
20
21
  if (!globalVar) {
21
22
  console.error(chalk_1.default.red(`Global variable "${globalKey}" not found.`));
22
- console.log(chalk_1.default.gray('Create it with: solidactions env:set ' + globalKey + ' <value>'));
23
+ console.log(chalk_1.default.gray('Create it with: solidactions env set ' + globalKey + ' <value>'));
23
24
  process.exit(1);
24
25
  }
26
+ // Check if project key already has a mapping
27
+ if (!options.yes) {
28
+ const mappingsResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectName}/variable-mappings`, { headers: (0, api_1.getApiHeaders)(config) });
29
+ const mappings = mappingsResponse.data || [];
30
+ const existing = mappings.find((m) => m.env_name === projectKey);
31
+ if (existing && (existing.has_value || existing.global_variable_id)) {
32
+ const currentSource = existing.global_variable_id
33
+ ? `mapped to global "${existing.global_variable_key || 'unknown'}"`
34
+ : 'has a local value';
35
+ console.log(chalk_1.default.yellow(`"${projectKey}" already ${currentSource} in "${projectName}".`));
36
+ const confirm = await (0, prompts_1.default)({
37
+ type: 'confirm',
38
+ name: 'proceed',
39
+ message: 'Overwrite mapping?',
40
+ initial: false,
41
+ });
42
+ if (!confirm.proceed) {
43
+ console.log(chalk_1.default.gray('Cancelled.'));
44
+ return;
45
+ }
46
+ }
47
+ }
25
48
  // Create the mapping
26
49
  await axios_1.default.post(`${config.host}/api/v1/projects/${projectName}/variable-mappings`, {
27
50
  project_key: projectKey,
@@ -147,7 +147,7 @@ async function envPull(projectName, options = {}) {
147
147
  // Full pull: Build .env file content
148
148
  const lines = [];
149
149
  lines.push(`# Environment variables for ${projectName} (${environment})`);
150
- lines.push(`# Generated by solidactions env:pull on ${new Date().toISOString()}`);
150
+ lines.push(`# Generated by solidactions env pull on ${new Date().toISOString()}`);
151
151
  lines.push('');
152
152
  let count = 0;
153
153
  let secretCount = 0;
@@ -208,7 +208,7 @@ async function envPull(projectName, options = {}) {
208
208
  else if (error.response.status === 404) {
209
209
  console.error(chalk_1.default.red(`Project "${projectSlug}" not found.`));
210
210
  if (environment !== 'production') {
211
- console.log(chalk_1.default.gray(`Try deploying with: solidactions deploy ${projectName} --env ${environment} --create`));
211
+ console.log(chalk_1.default.gray(`Try deploying with: solidactions project deploy ${projectName} --env ${environment} --create`));
212
212
  }
213
213
  }
214
214
  else {
@@ -92,7 +92,7 @@ async function envPush(projectName, sourcePath, options = {}) {
92
92
  }
93
93
  else if (error.response?.status === 404) {
94
94
  console.error(chalk_1.default.red(`Project "${projectSlug}" not found.`));
95
- console.log(chalk_1.default.gray(`Deploy first with: solidactions deploy ${projectName} --env ${environment}` + (environment !== 'production' ? ' --create' : '')));
95
+ console.log(chalk_1.default.gray(`Deploy first with: solidactions project deploy ${projectName} --env ${environment}` + (environment !== 'production' ? ' --create' : '')));
96
96
  process.exit(1);
97
97
  }
98
98
  throw error;
@@ -6,13 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.envSet = envSet;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = __importDefault(require("prompts"));
9
10
  const api_1 = require("../utils/api");
10
11
  async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
11
12
  const config = await (0, api_1.requireConfigWithWorkspace)();
12
13
  // Detect mode based on arguments
13
14
  const isProjectMode = valueIfProject !== undefined;
14
15
  if (isProjectMode) {
15
- // Project mode: solidactions env:set <project> <key> <value>
16
+ // Project mode: solidactions env set <project> <key> <value>
16
17
  const projectName = keyOrProject;
17
18
  const key = valueOrKey;
18
19
  const value = valueIfProject;
@@ -24,6 +25,25 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
24
25
  // Auto-detect secrets
25
26
  const isSecret = options.secret || /secret|key|token|password|credential/i.test(key);
26
27
  try {
28
+ // Check if variable already has a value
29
+ if (!options.yes) {
30
+ const mappingsResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?reveal=true`, { headers: (0, api_1.getApiHeaders)(config) });
31
+ const mappings = mappingsResponse.data || [];
32
+ const existing = mappings.find((m) => m.env_name === key);
33
+ if (existing && existing.has_value) {
34
+ console.log(chalk_1.default.yellow(`Variable "${key}" already has a value in "${projectName}" (${environment}).`));
35
+ const confirm = await (0, prompts_1.default)({
36
+ type: 'confirm',
37
+ name: 'proceed',
38
+ message: 'Overwrite?',
39
+ initial: false,
40
+ });
41
+ if (!confirm.proceed) {
42
+ console.log(chalk_1.default.gray('Cancelled.'));
43
+ return;
44
+ }
45
+ }
46
+ }
27
47
  // Use bulk endpoint for upsert
28
48
  const response = await axios_1.default.post(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings/bulk`, {
29
49
  variables: [{
@@ -60,7 +80,7 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
60
80
  }
61
81
  }
62
82
  else {
63
- // Global mode: solidactions env:set <key> <value>
83
+ // Global mode: solidactions env set <key> <value>
64
84
  const key = keyOrProject;
65
85
  const value = valueOrKey;
66
86
  const isSecret = options.secret || false;
@@ -98,6 +118,19 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
98
118
  const existing = variables.find((v) => v.key === key);
99
119
  let action;
100
120
  if (existing) {
121
+ if (!options.yes) {
122
+ console.log(chalk_1.default.yellow(`Global variable "${key}" already exists.`));
123
+ const confirm = await (0, prompts_1.default)({
124
+ type: 'confirm',
125
+ name: 'proceed',
126
+ message: 'Overwrite?',
127
+ initial: false,
128
+ });
129
+ if (!confirm.proceed) {
130
+ console.log(chalk_1.default.gray('Cancelled.'));
131
+ return;
132
+ }
133
+ }
101
134
  await axios_1.default.put(`${config.host}/api/v1/variables/${existing.id}`, body, {
102
135
  headers: (0, api_1.getApiHeaders)(config, 'application/json'),
103
136
  });
@@ -83,13 +83,13 @@ async function init(apiKey, options) {
83
83
  }
84
84
  }
85
85
  catch {
86
- console.log(chalk_1.default.yellow('Could not set workspace. Run `solidactions workspace:set` later.'));
86
+ console.log(chalk_1.default.yellow('Could not set workspace. Run `solidactions workspace set` later.'));
87
87
  }
88
88
  console.log('');
89
89
  console.log(chalk_1.default.blue('Quick start:'));
90
- console.log(chalk_1.default.gray(' solidactions deploy <project-name> Deploy current directory'));
91
- console.log(chalk_1.default.gray(' solidactions run <project> <workflow> Run a workflow'));
92
- console.log(chalk_1.default.gray(' solidactions runs List recent runs'));
90
+ console.log(chalk_1.default.gray(' solidactions project deploy <name> Deploy current directory'));
91
+ console.log(chalk_1.default.gray(' solidactions run start <proj> <wf> Run a workflow'));
92
+ console.log(chalk_1.default.gray(' solidactions run list List recent runs'));
93
93
  }
94
94
  async function logout() {
95
95
  if (fs_1.default.existsSync(CONFIG_FILE)) {
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.projectList = projectList;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const api_1 = require("../utils/api");
10
+ async function projectList() {
11
+ const config = await (0, api_1.requireConfigWithWorkspace)();
12
+ console.log(chalk_1.default.blue('Projects:\n'));
13
+ try {
14
+ const response = await axios_1.default.get(`${config.host}/api/v1/projects`, {
15
+ headers: (0, api_1.getApiHeaders)(config),
16
+ });
17
+ const projects = response.data.data || [];
18
+ if (projects.length === 0) {
19
+ console.log(chalk_1.default.gray('No projects found.'));
20
+ return;
21
+ }
22
+ console.log(chalk_1.default.gray('NAME'.padEnd(25) + 'STATUS'.padEnd(15) + 'SNAPSHOT'.padEnd(30) + 'ENVIRONMENTS'));
23
+ console.log(chalk_1.default.gray('-'.repeat(100)));
24
+ for (const project of projects) {
25
+ const name = (project.name || '?').padEnd(25);
26
+ const status = project.status || '?';
27
+ const statusColor = status === 'ready' ? chalk_1.default.green : status === 'building' ? chalk_1.default.yellow : chalk_1.default.gray;
28
+ const snapshot = (project.snapshot_name || '-').padEnd(30);
29
+ const envs = (project.environments || [project.environment || 'production']).join(', ');
30
+ console.log(name +
31
+ statusColor(status.padEnd(15)) +
32
+ chalk_1.default.gray(snapshot) +
33
+ chalk_1.default.gray(envs));
34
+ }
35
+ console.log('');
36
+ console.log(chalk_1.default.gray(`${projects.length} project(s)`));
37
+ }
38
+ catch (error) {
39
+ if (error.response) {
40
+ if (error.response.status === 401) {
41
+ console.error(chalk_1.default.red('Authentication failed. Run "solidactions init <api-key>" to re-configure.'));
42
+ }
43
+ else {
44
+ console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
45
+ }
46
+ }
47
+ else {
48
+ console.error(chalk_1.default.red('Connection failed:'), error.message);
49
+ }
50
+ process.exit(1);
51
+ }
52
+ }
@@ -6,30 +6,47 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.pull = pull;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const zlib_1 = require("zlib");
10
+ const promises_1 = require("stream/promises");
11
+ const stream_1 = require("stream");
12
+ const tar_1 = require("tar");
9
13
  const axios_1 = __importDefault(require("axios"));
10
14
  const chalk_1 = __importDefault(require("chalk"));
11
- const adm_zip_1 = __importDefault(require("adm-zip"));
15
+ const prompts_1 = __importDefault(require("prompts"));
12
16
  const api_1 = require("../utils/api");
13
- async function pull(projectName, destPath) {
17
+ async function pull(projectName, destPath, options = {}) {
14
18
  const config = await (0, api_1.requireConfigWithWorkspace)();
15
19
  const destination = destPath ? path_1.default.resolve(destPath) : process.cwd();
20
+ // Warn if destination has existing files
21
+ if (fs_1.default.existsSync(destination)) {
22
+ const entries = fs_1.default.readdirSync(destination);
23
+ if (entries.length > 0 && !options.yes) {
24
+ console.log(chalk_1.default.yellow(`Destination "${destination}" is not empty (${entries.length} items).`));
25
+ console.log(chalk_1.default.yellow('Pulling will overwrite existing files.'));
26
+ const response = await (0, prompts_1.default)({
27
+ type: 'confirm',
28
+ name: 'proceed',
29
+ message: 'Continue?',
30
+ initial: false,
31
+ });
32
+ if (!response.proceed) {
33
+ console.log(chalk_1.default.gray('Cancelled.'));
34
+ process.exit(0);
35
+ }
36
+ }
37
+ }
16
38
  console.log(chalk_1.default.blue(`Pulling project "${projectName}"...`));
17
39
  try {
18
40
  const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectName}/source`, {
19
41
  headers: { ...(0, api_1.getApiHeaders)(config), 'Accept': 'application/octet-stream' },
20
42
  responseType: 'arraybuffer',
21
43
  });
22
- const zipBuffer = Buffer.from(response.data);
23
- const tempZipPath = path_1.default.join(destination, '.steps-pull-temp.zip');
24
- // Write the zip to a temp file
25
- fs_1.default.writeFileSync(tempZipPath, zipBuffer);
26
- console.log(chalk_1.default.gray(`Downloaded ${zipBuffer.length} bytes`));
44
+ const buffer = Buffer.from(response.data);
45
+ console.log(chalk_1.default.gray(`Downloaded ${buffer.length} bytes`));
27
46
  console.log(chalk_1.default.yellow(`Extracting to ${destination}...`));
28
- // Extract the zip
29
- const zip = new adm_zip_1.default(tempZipPath);
30
- zip.extractAllTo(destination, true);
31
- // Clean up temp file
32
- fs_1.default.unlinkSync(tempZipPath);
47
+ fs_1.default.mkdirSync(destination, { recursive: true });
48
+ const readable = stream_1.Readable.from(buffer);
49
+ await (0, promises_1.pipeline)(readable, (0, zlib_1.createGunzip)(), (0, tar_1.extract)({ cwd: destination, strip: 0 }));
33
50
  console.log(chalk_1.default.green(`Project "${projectName}" pulled successfully!`));
34
51
  }
35
52
  catch (error) {
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runs = runs;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const api_1 = require("../utils/api");
10
+ async function runs(projectName, options = {}) {
11
+ const config = await (0, api_1.requireConfigWithWorkspace)();
12
+ // Default limit is lower for --detailed (more data per run)
13
+ const defaultLimit = options.detailed ? 5 : 20;
14
+ const limit = options.limit || defaultLimit;
15
+ try {
16
+ const params = { limit };
17
+ if (projectName)
18
+ params.project = projectName;
19
+ if (options.offset)
20
+ params.offset = options.offset;
21
+ if (options.status)
22
+ params.status = options.status;
23
+ if (options.since)
24
+ params.since = parseSince(options.since);
25
+ if (options.workflow)
26
+ params.workflow = options.workflow;
27
+ if (options.detailed)
28
+ params.detailed = '1';
29
+ const response = await axios_1.default.get(`${config.host}/api/v1/runs`, {
30
+ headers: (0, api_1.getApiHeaders)(config),
31
+ params,
32
+ });
33
+ const runsList = response.data.data || response.data;
34
+ if (!runsList || runsList.length === 0) {
35
+ if (options.json) {
36
+ console.log('[]');
37
+ }
38
+ else {
39
+ console.log(chalk_1.default.gray('No runs found.'));
40
+ }
41
+ return;
42
+ }
43
+ if (options.json) {
44
+ console.log(JSON.stringify(runsList, null, 2));
45
+ return;
46
+ }
47
+ // Human-readable output
48
+ if (options.detailed) {
49
+ displayDetailedList(runsList, projectName);
50
+ }
51
+ else {
52
+ displaySummaryTable(runsList, projectName);
53
+ }
54
+ }
55
+ catch (error) {
56
+ if (error.response) {
57
+ if (error.response.status === 401) {
58
+ console.error(chalk_1.default.red('Authentication failed. Run "solidactions init <api-key>" to re-configure.'));
59
+ }
60
+ else {
61
+ console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
62
+ }
63
+ }
64
+ else {
65
+ console.error(chalk_1.default.red('Connection failed:'), error.message);
66
+ }
67
+ process.exit(1);
68
+ }
69
+ }
70
+ // ─── Display modes ─────────────────────────────────────────────────────────
71
+ function displaySummaryTable(runsList, projectName) {
72
+ const header = projectName ? `Recent runs for "${projectName}":` : 'Recent runs:';
73
+ console.log(chalk_1.default.blue(header));
74
+ console.log('');
75
+ console.log(chalk_1.default.gray('ID'.padEnd(8) + 'WORKFLOW'.padEnd(25) + 'STATUS'.padEnd(12) + 'TRIGGERED'.padEnd(22) + 'TRIGGERED BY'));
76
+ console.log(chalk_1.default.gray('-'.repeat(90)));
77
+ for (const run of runsList) {
78
+ const id = String(run.id || '?').padEnd(8);
79
+ const workflow = truncate(run.workflow_name || '?', 24).padEnd(25);
80
+ const status = run.execution_status || run.status || '?';
81
+ const statusColor = getStatusColor(status);
82
+ const triggeredAt = run.triggered_at ? new Date(run.triggered_at).toLocaleString() : '-';
83
+ const triggeredBy = run.triggered_by || '-';
84
+ console.log(chalk_1.default.gray(id) +
85
+ workflow +
86
+ statusColor(status.padEnd(12)) +
87
+ chalk_1.default.gray(triggeredAt.padEnd(22)) +
88
+ chalk_1.default.gray(triggeredBy));
89
+ }
90
+ console.log('');
91
+ console.log(chalk_1.default.gray(`Showing ${runsList.length} run(s)`));
92
+ }
93
+ function displayDetailedList(runsList, projectName) {
94
+ const header = projectName ? `Runs for "${projectName}" (detailed):` : 'Runs (detailed):';
95
+ console.log(chalk_1.default.blue(header));
96
+ for (const run of runsList) {
97
+ const status = run.execution_status || run.status || '?';
98
+ const statusColor = getStatusColor(status);
99
+ const exitStr = run.exit_code !== null && run.exit_code !== undefined ? ` (exit ${run.exit_code})` : '';
100
+ console.log('');
101
+ console.log(chalk_1.default.bold(` Run #${run.id}`) + chalk_1.default.gray(` — ${run.workflow_name || '?'} (${run.project_name || '?'})`));
102
+ console.log(` Status: ${statusColor(status)}${chalk_1.default.gray(exitStr)}`);
103
+ console.log(` Trigger: ${chalk_1.default.gray(run.triggered_by || '-')}`);
104
+ // Timeline
105
+ if (run.timeline) {
106
+ const formatTs = (iso) => iso ? new Date(iso).toLocaleString() : '-';
107
+ console.log(` Triggered: ${chalk_1.default.gray(formatTs(run.timeline.triggered))}`);
108
+ console.log(` Started: ${chalk_1.default.gray(formatTs(run.timeline.started))}`);
109
+ console.log(` Completed: ${chalk_1.default.gray(formatTs(run.timeline.completed))}`);
110
+ }
111
+ // Steps summary
112
+ 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)`)}`);
116
+ }
117
+ // Logs snippet (first errors or last 3 lines)
118
+ if (run.logs && typeof run.logs === 'string' && run.logs.trim()) {
119
+ const lines = run.logs.trim().split('\n');
120
+ const errorLines = lines.filter((l) => /error|fail|exception/i.test(l));
121
+ if (errorLines.length > 0) {
122
+ console.log(` Errors:`);
123
+ for (const line of errorLines.slice(0, 3)) {
124
+ console.log(chalk_1.default.red(` ${truncate(line, 80)}`));
125
+ }
126
+ }
127
+ }
128
+ }
129
+ console.log('');
130
+ console.log(chalk_1.default.gray(`Showing ${runsList.length} run(s)`));
131
+ }
132
+ // ─── Utility ───────────────────────────────────────────────────────────────
133
+ /**
134
+ * Parse --since value into epoch ms.
135
+ * Accepts: "1h", "30m", "2d", "1w", or ISO date string.
136
+ */
137
+ function parseSince(since) {
138
+ const match = since.match(/^(\d+)(m|h|d|w)$/);
139
+ if (match) {
140
+ const value = parseInt(match[1]);
141
+ const unit = match[2];
142
+ const multipliers = {
143
+ m: 60 * 1000,
144
+ h: 60 * 60 * 1000,
145
+ d: 24 * 60 * 60 * 1000,
146
+ w: 7 * 24 * 60 * 60 * 1000,
147
+ };
148
+ const ms = Date.now() - (value * multipliers[unit]);
149
+ return String(ms);
150
+ }
151
+ // Assume ISO date string — let the backend parse it
152
+ return since;
153
+ }
154
+ function formatDuration(ms) {
155
+ if (ms === null || ms === undefined)
156
+ return '-';
157
+ if (ms < 1000)
158
+ return `${ms}ms`;
159
+ if (ms < 60000)
160
+ return `${(ms / 1000).toFixed(1)}s`;
161
+ if (ms < 3600000)
162
+ return `${Math.round(ms / 60000)}m`;
163
+ return `${Math.round(ms / 3600000)}h`;
164
+ }
165
+ function truncate(str, max) {
166
+ if (str.length <= max)
167
+ return str;
168
+ return str.substring(0, max - 3) + '...';
169
+ }
170
+ function getStatusColor(status) {
171
+ switch (status?.toLowerCase()) {
172
+ case 'completed':
173
+ case 'success':
174
+ return chalk_1.default.green;
175
+ case 'running':
176
+ return chalk_1.default.blue;
177
+ case 'pending':
178
+ case 'queued':
179
+ return chalk_1.default.yellow;
180
+ case 'failed':
181
+ case 'error':
182
+ return chalk_1.default.red;
183
+ default:
184
+ return chalk_1.default.gray;
185
+ }
186
+ }