@solidactions/cli 1.23.0 → 1.24.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.
@@ -7,15 +7,17 @@ exports.getConfig = getConfig;
7
7
  exports.saveConfig = saveConfig;
8
8
  exports.clearConfig = clearConfig;
9
9
  exports.resolveLoginHost = resolveLoginHost;
10
+ exports.backupPathFor = backupPathFor;
10
11
  exports.loginHostLines = loginHostLines;
11
12
  exports.login = login;
12
13
  exports.logout = logout;
13
14
  exports.whoami = whoami;
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const readline_1 = __importDefault(require("readline"));
14
17
  const chalk_1 = __importDefault(require("chalk"));
15
- const api_1 = require("../utils/api");
16
- const workspaces_1 = require("./workspaces");
17
18
  const config_1 = require("../utils/config");
18
19
  const config_write_target_1 = require("../utils/config-write-target");
20
+ const workspace_lookup_1 = require("../utils/workspace-lookup");
19
21
  function getConfig() {
20
22
  const resolved = (0, config_1.resolveConfig)();
21
23
  return resolved ? resolved.config : null;
@@ -37,6 +39,18 @@ function resolveLoginHost(options) {
37
39
  }
38
40
  return { host: 'https://app.solidactions.com', isDefault: true };
39
41
  }
42
+ /**
43
+ * Path for a timestamped backup of `targetPath`, e.g.
44
+ * `config.json.bak-2026-07-05T12-30-00Z`. Pure — takes `now` so tests are
45
+ * deterministic.
46
+ */
47
+ function backupPathFor(targetPath, now = new Date()) {
48
+ const timestamp = now.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '-');
49
+ return `${targetPath}.bak-${timestamp}`;
50
+ }
51
+ const LOGIN_REFUSAL_MESSAGE = 'Refusing to write config non-interactively. Pass --global to update the machine-wide config ' +
52
+ 'at ~/.solidactions/config.json (a backup is kept if one exists), or --local to write ' +
53
+ './.solidactions/config.json for just this directory.';
40
54
  function loginHostLines(resolved) {
41
55
  if (resolved.isDefault) {
42
56
  return [
@@ -46,6 +60,36 @@ function loginHostLines(resolved) {
46
60
  }
47
61
  return [`Host: ${resolved.host}`];
48
62
  }
63
+ /**
64
+ * Prompt the user to pick a workspace from an already-fetched list. Auto-
65
+ * selects when there's exactly one. Returns undefined if none exist.
66
+ */
67
+ async function selectWorkspaceInteractively(workspaces) {
68
+ if (workspaces.length === 0) {
69
+ console.log(chalk_1.default.yellow('No workspaces found. Create one at your SolidActions dashboard, then run `solidactions workspace set <name>`.'));
70
+ return undefined;
71
+ }
72
+ if (workspaces.length === 1) {
73
+ console.log(chalk_1.default.gray(`Auto-selected workspace: ${workspaces[0].name}`));
74
+ return workspaces[0];
75
+ }
76
+ console.log(chalk_1.default.blue('\nSelect a workspace:\n'));
77
+ workspaces.forEach((ws, i) => {
78
+ console.log(` ${chalk_1.default.white(`${i + 1}.`)} ${ws.name}`);
79
+ });
80
+ console.log('');
81
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
82
+ const answer = await new Promise((resolve) => {
83
+ rl.question(chalk_1.default.blue('Enter number: '), resolve);
84
+ });
85
+ rl.close();
86
+ const index = parseInt(answer, 10) - 1;
87
+ if (isNaN(index) || index < 0 || index >= workspaces.length) {
88
+ console.error(chalk_1.default.red('Invalid selection.'));
89
+ process.exit(1);
90
+ }
91
+ return workspaces[index];
92
+ }
49
93
  async function login(apiKey, options) {
50
94
  const resolved = resolveLoginHost(options);
51
95
  const host = resolved.host;
@@ -54,39 +98,71 @@ async function login(apiKey, options) {
54
98
  console.log(chalk_1.default.gray('Generate an API key at: ') + chalk_1.default.blue(`${host}/settings/api-keys`));
55
99
  process.exit(1);
56
100
  }
57
- // Determine target location.
58
- const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global });
59
- const targetPath = (0, config_write_target_1.pathForTarget)(target);
60
101
  console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
61
102
  for (const line of loginHostLines(resolved)) {
62
103
  console.log(resolved.isDefault ? chalk_1.default.yellow(line) : chalk_1.default.gray(line));
63
104
  }
64
- if ((0, config_1.readConfigFile)(targetPath)) {
65
- console.log(chalk_1.default.yellow(`Existing config at ${targetPath} will be overwritten.`));
66
- }
67
105
  const config = {
68
106
  host,
69
107
  apiKey: apiKey.trim(),
70
108
  };
71
- (0, config_1.writeConfigFile)(targetPath, config);
72
- if (target === 'local') {
73
- await (0, config_write_target_1.ensureGitignoreCovers)(process.cwd(), !!options.gitignore);
74
- }
75
- console.log(chalk_1.default.green('Logged in successfully!'));
76
- console.log(chalk_1.default.gray(`Configuration saved to ${targetPath}`));
77
- console.log('');
78
- // Workspace selection — ensureWorkspaceSelected writes to the right file via the resolver.
109
+ // 1. Validate the key BEFORE any disk write.
110
+ let workspaces;
79
111
  try {
80
- if (options.workspace) {
81
- await (0, workspaces_1.workspaceSet)(options.workspace);
112
+ workspaces = await (0, workspace_lookup_1.fetchWorkspaces)(config);
113
+ }
114
+ catch (e) {
115
+ if (e.response?.status === 401) {
116
+ console.error(chalk_1.default.red(`Invalid API key for ${host}.`));
82
117
  }
83
118
  else {
84
- await (0, api_1.ensureWorkspaceSelected)(config);
119
+ console.error(chalk_1.default.red(`Could not reach ${host}: ${e.message}`));
120
+ }
121
+ process.exit(1);
122
+ return;
123
+ }
124
+ // 2. Resolve the workspace (still before writing).
125
+ if (options.workspace) {
126
+ const match = (0, workspace_lookup_1.matchWorkspace)(options.workspace, workspaces);
127
+ if (!match) {
128
+ console.error(chalk_1.default.red(`Workspace "${options.workspace}" not found. Run \`solidactions workspace list\` to list available workspaces.`));
129
+ process.exit(1);
130
+ return;
131
+ }
132
+ config.workspace = match.slug ?? match.name;
133
+ config.workspaceId = match.id;
134
+ }
135
+ else if (process.stdin.isTTY) {
136
+ const selected = await selectWorkspaceInteractively(workspaces);
137
+ if (selected) {
138
+ config.workspace = selected.slug ?? selected.name;
139
+ config.workspaceId = selected.id;
85
140
  }
86
141
  }
87
- catch {
88
- console.log(chalk_1.default.yellow('Could not set workspace. Run `solidactions workspace set` later.'));
142
+ else {
143
+ console.log(chalk_1.default.yellow('No workspace set run `solidactions workspace set <name>` later.'));
89
144
  }
145
+ // 3. Determine target location, back up if needed, and write ONCE.
146
+ const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global }, undefined, LOGIN_REFUSAL_MESSAGE);
147
+ const targetPath = (0, config_write_target_1.pathForTarget)(target);
148
+ const existingRaw = fs_1.default.existsSync(targetPath) ? fs_1.default.readFileSync(targetPath, 'utf-8') : null;
149
+ const wouldChange = existingRaw !== null && existingRaw.trim() !== JSON.stringify(config, null, 2).trim();
150
+ if (wouldChange) {
151
+ const backupPath = backupPathFor(targetPath);
152
+ const proceed = await (0, config_write_target_1.confirmOverwrite)(targetPath, backupPath);
153
+ if (!proceed) {
154
+ console.log(chalk_1.default.yellow('Aborted. No changes were made.'));
155
+ process.exit(0);
156
+ }
157
+ fs_1.default.copyFileSync(targetPath, backupPath);
158
+ console.log(chalk_1.default.gray(`Backup saved to ${backupPath}`));
159
+ }
160
+ (0, config_1.writeConfigFile)(targetPath, config);
161
+ if (target === 'local') {
162
+ await (0, config_write_target_1.ensureGitignoreCovers)(process.cwd(), !!options.gitignore);
163
+ }
164
+ console.log(chalk_1.default.green('Logged in successfully!'));
165
+ console.log(chalk_1.default.gray(`Configuration saved to ${targetPath}`));
90
166
  console.log('');
91
167
  console.log(chalk_1.default.blue('Next step — scaffold a new project (includes AI tooling):'));
92
168
  console.log(chalk_1.default.gray(' solidactions init <project-name> Creates ./<project-name>/ with scaffold + AI skills'));
@@ -123,7 +199,8 @@ function logout(options = {}) {
123
199
  console.log(chalk_1.default.green(`Logged out. Removed ${targetPath}`));
124
200
  }
125
201
  else {
126
- console.log(chalk_1.default.gray(`Not logged in (no config at ${targetPath}).`));
202
+ console.log(chalk_1.default.gray(`Not logged in (no config at ${targetPath}) — nothing to remove.`));
203
+ process.exit(0);
127
204
  }
128
205
  }
129
206
  function whoami() {
@@ -151,8 +228,10 @@ function whoami() {
151
228
  : config.workspaceId
152
229
  ? `${config.workspaceId} (slug unknown — run 'workspace set <slug>' to populate)`
153
230
  : '';
231
+ const isFileSource = (src) => src !== null && src !== 'env' && src !== 'cli';
232
+ const workspaceInheritedFromOtherFile = isFileSource(sources.workspaceId) && sources.workspaceId !== sources.apiKey;
154
233
  console.log(chalk_1.default.blue('Current configuration:'));
155
234
  console.log(` Host: ${config.host.padEnd(50)} ${fmt(sources.host)}`);
156
235
  console.log(` API Key: ${maskedKey.padEnd(50)} ${fmt(sources.apiKey)}`);
157
- console.log(` Workspace: ${workspaceLabel.padEnd(50)} ${fmt(sources.workspaceId)}`);
236
+ console.log(` Workspace: ${workspaceLabel.padEnd(50)} ${fmt(sources.workspaceId)}${workspaceInheritedFromOtherFile ? chalk_1.default.yellow(' (inherited from a different config file)') : ''}`);
158
237
  }
@@ -23,7 +23,7 @@ async function oauthActionsList(platform, options) {
23
23
  return;
24
24
  }
25
25
  if (actions.length === 0) {
26
- console.log('No actions found.');
26
+ console.log(`No actions found for "${platform}" — check the platform name and that a connection exists (\`solidactions env pull --update-oauth\` syncs connections).`);
27
27
  return;
28
28
  }
29
29
  for (const a of actions) {
@@ -27,7 +27,7 @@ async function oauthActionsSearch(platform, query, options) {
27
27
  return;
28
28
  }
29
29
  if (actions.length === 0) {
30
- console.log('No actions found.');
30
+ console.log(`No actions found for "${platform}" — check the platform name and that a connection exists (\`solidactions env pull --update-oauth\` syncs connections).`);
31
31
  return;
32
32
  }
33
33
  for (const a of actions) {
@@ -7,30 +7,36 @@ exports.projectList = projectList;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const api_1 = require("../utils/api");
10
- async function projectList() {
10
+ const table_1 = require("../utils/table");
11
+ async function projectList(options = {}) {
11
12
  const config = await (0, api_1.requireConfigWithWorkspace)();
12
- console.log(chalk_1.default.blue('Projects:\n'));
13
13
  try {
14
14
  const response = await axios_1.default.get(`${config.host}/api/v1/projects`, {
15
15
  headers: (0, api_1.getApiHeaders)(config),
16
16
  });
17
17
  const projects = response.data.data || [];
18
+ if (options.json) {
19
+ console.log(JSON.stringify(projects, null, 2));
20
+ return;
21
+ }
22
+ // Header is only printed after the fetch succeeds — an auth/network
23
+ // failure used to print "Projects:" followed immediately by the error.
24
+ console.log(chalk_1.default.blue('Projects:\n'));
18
25
  if (projects.length === 0) {
19
26
  console.log(chalk_1.default.gray('No projects found.'));
20
27
  return;
21
28
  }
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 rows = projects.map((project) => {
29
30
  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));
31
+ return [project.name || '?', project.status || '?', project.snapshot_name || '-', envs];
32
+ });
33
+ const lines = (0, table_1.renderTable)(['NAME', 'STATUS', 'SNAPSHOT', 'ENVIRONMENTS'], rows, {
34
+ minWidths: [25, 15, 30],
35
+ });
36
+ console.log(chalk_1.default.gray(lines[0]));
37
+ console.log(chalk_1.default.gray(lines[1]));
38
+ for (const line of lines.slice(2)) {
39
+ console.log(line);
34
40
  }
35
41
  console.log('');
36
42
  console.log(chalk_1.default.gray(`${projects.length} project(s)`));
@@ -45,8 +45,23 @@ async function pull(projectName, destPath, options = {}) {
45
45
  console.log(chalk_1.default.gray(`Downloaded ${buffer.length} bytes`));
46
46
  console.log(chalk_1.default.yellow(`Extracting to ${destination}...`));
47
47
  fs_1.default.mkdirSync(destination, { recursive: true });
48
+ // The server streams back the exact archive `deploy` uploaded: a
49
+ // root-level Dockerfile plus every project file nested under
50
+ // tenantcode/ (see deploy.ts:510-527). Unwrap it here so the pulled
51
+ // project's files land at dest root, matching what the user actually
52
+ // wrote — dropping the generated Dockerfile and any noCache
53
+ // cache-buster entries (deploy.ts's cacheBusterEntry).
48
54
  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 }));
55
+ await (0, promises_1.pipeline)(readable, (0, zlib_1.createGunzip)(), (0, tar_1.extract)({
56
+ cwd: destination,
57
+ strip: 1,
58
+ // `filter` receives the pre-strip path (verified against this
59
+ // version of the `tar` package).
60
+ filter: (entryPath) => {
61
+ const top = entryPath.split('/')[0];
62
+ return top === 'tenantcode' && !/\/sa-nocache-[0-9a-f-]+$/.test(entryPath);
63
+ },
64
+ }));
50
65
  console.log(chalk_1.default.green(`Project "${projectName}" pulled successfully!`));
51
66
  }
52
67
  catch (error) {
@@ -69,7 +69,7 @@ async function rolePushWithConfig(dir, options, config) {
69
69
  readResult = await (0, mcp_1.callCrewsTool)(config, 'roles', { action: 'read', name });
70
70
  }
71
71
  catch (e) {
72
- process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
72
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
73
73
  process.exit(1);
74
74
  }
75
75
  if (!readResult.ok) {
@@ -110,7 +110,7 @@ async function rolePushWithConfig(dir, options, config) {
110
110
  });
111
111
  }
112
112
  catch (e) {
113
- process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
113
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
114
114
  process.exit(1);
115
115
  }
116
116
  // On name collision, switch to the edit path.
@@ -126,7 +126,7 @@ async function rolePushWithConfig(dir, options, config) {
126
126
  });
127
127
  }
128
128
  catch (e) {
129
- process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
129
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
130
130
  process.exit(1);
131
131
  }
132
132
  if (!result.ok) {
@@ -10,6 +10,7 @@ exports.detailedOutcomeTag = detailedOutcomeTag;
10
10
  const axios_1 = __importDefault(require("axios"));
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
12
  const api_1 = require("../utils/api");
13
+ const table_1 = require("../utils/table");
13
14
  /**
14
15
  * Build the params object for GET /api/v1/runs.
15
16
  * Extracted as a pure function so it can be unit-tested without mocking axios.
@@ -124,19 +125,30 @@ function displaySummaryTable(runsList, projectName) {
124
125
  const header = projectName ? `Recent runs for "${projectName}":` : 'Recent runs:';
125
126
  console.log(chalk_1.default.blue(header));
126
127
  console.log('');
127
- console.log(chalk_1.default.gray('ID'.padEnd(8) + 'WORKFLOW'.padEnd(25) + 'STATUS'.padEnd(12) + 'TRIGGERED'.padEnd(22) + 'TRIGGERED BY'));
128
- console.log(chalk_1.default.gray('-'.repeat(90)));
129
- for (const run of runsList) {
130
- const id = String(run.id || '?').padEnd(8);
131
- const workflow = truncate(run.workflow_name || '?', 24).padEnd(25);
132
- const { label: status, attention } = summaryStatusLabel(run);
133
- const statusColor = attention ? chalk_1.default.yellow : getStatusColor(status);
128
+ const headers = ['ID', 'WORKFLOW', 'STATUS', 'TRIGGERED', 'TRIGGERED BY'];
129
+ const rows = runsList.map((run) => {
130
+ const { label: status } = summaryStatusLabel(run);
134
131
  const triggeredAt = run.triggered_at ? new Date(run.triggered_at).toLocaleString() : '-';
135
- const triggeredBy = run.triggered_by || '-';
136
- console.log(chalk_1.default.gray(id) +
137
- workflow +
138
- statusColor(status.padEnd(12)) +
139
- chalk_1.default.gray(triggeredAt.padEnd(22)) +
132
+ return [
133
+ String(run.id || '?'),
134
+ (0, table_1.sanitizeCell)((0, table_1.truncateCell)(run.workflow_name || '?', 24)),
135
+ status,
136
+ triggeredAt,
137
+ (0, table_1.sanitizeCell)(run.triggered_by || '-'),
138
+ ];
139
+ });
140
+ const widths = (0, table_1.computeColumnWidths)(headers, rows, { minWidths: [8, 25, 12, 22, 0] });
141
+ console.log(chalk_1.default.gray(headers.map((h, i) => h.padEnd(widths[i])).join('')));
142
+ console.log(chalk_1.default.gray('-'.repeat(widths.reduce((a, b) => a + b, 0))));
143
+ for (let i = 0; i < runsList.length; i++) {
144
+ const run = runsList[i];
145
+ const [id, workflow, status, triggeredAt, triggeredBy] = rows[i];
146
+ const { attention } = summaryStatusLabel(run);
147
+ const statusColor = attention ? chalk_1.default.yellow : getStatusColor(status);
148
+ console.log(chalk_1.default.gray(id.padEnd(widths[0])) +
149
+ workflow.padEnd(widths[1]) +
150
+ statusColor(status.padEnd(widths[2])) +
151
+ chalk_1.default.gray(triggeredAt.padEnd(widths[3])) +
140
152
  chalk_1.default.gray(triggeredBy));
141
153
  }
142
154
  console.log('');
@@ -72,7 +72,13 @@ async function run(projectName, workflowName, options) {
72
72
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
73
73
  }
74
74
  else if (error.response.status === 404) {
75
- console.error(chalk_1.default.red('Project or workflow not found.'));
75
+ const envsList = await (0, api_1.describeProjectEnvironments)(config, projectName);
76
+ if (envsList) {
77
+ console.error(chalk_1.default.red(`Project "${projectName}" has no ${environment} environment (exists in: ${envsList}). Pass -e <env> to target a different environment.`));
78
+ }
79
+ else {
80
+ console.error(chalk_1.default.red('Project or workflow not found.'));
81
+ }
76
82
  }
77
83
  else if (error.response.status === 422) {
78
84
  console.error(chalk_1.default.red('Validation error:'), error.response.data.message);
@@ -12,6 +12,11 @@ const axios_1 = __importDefault(require("axios"));
12
12
  const chalk_1 = __importDefault(require("chalk"));
13
13
  const api_1 = require("../utils/api");
14
14
  async function runView(runId, options) {
15
+ if (!/^\d+$/.test(runId)) {
16
+ console.error(chalk_1.default.red(`Invalid run id: "${runId}" (run ids are numeric — see \`solidactions run list\`).`));
17
+ process.exit(1);
18
+ return;
19
+ }
15
20
  const config = await (0, api_1.requireConfigWithWorkspace)();
16
21
  try {
17
22
  // Fetch run data (includes session timing)
@@ -42,7 +42,7 @@ async function skillPullWithConfig(name, dest, options, config) {
42
42
  result = await (0, mcp_1.callCrewsTool)(config, 'skills', { action: 'read', identifier: name });
43
43
  }
44
44
  catch (e) {
45
- process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
45
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
46
46
  process.exit(1);
47
47
  }
48
48
  if (!result.ok) {
@@ -193,7 +193,7 @@ async function pushParsedSkill(payload, options, config) {
193
193
  readResult = await (0, mcp_1.callCrewsTool)(config, tool, readArgs);
194
194
  }
195
195
  catch (e) {
196
- throw new Error(`MCP request failed — ${e.message}`);
196
+ throw new Error(`${e.message}`);
197
197
  }
198
198
  if (!readResult.ok) {
199
199
  const code = readResult.data?.code;
@@ -215,7 +215,7 @@ async function pushParsedSkill(payload, options, config) {
215
215
  result = await (0, mcp_1.callCrewsTool)(config, tool, createArgs);
216
216
  }
217
217
  catch (e) {
218
- throw new Error(`MCP request failed — ${e.message}`);
218
+ throw new Error(`${e.message}`);
219
219
  }
220
220
  // On name collision, switch to the edit path. properties → properties_patch.
221
221
  if (!result.ok && result.data?.code === 'name_collision') {
@@ -226,7 +226,7 @@ async function pushParsedSkill(payload, options, config) {
226
226
  result = await (0, mcp_1.callCrewsTool)(config, tool, editArgs);
227
227
  }
228
228
  catch (e) {
229
- throw new Error(`MCP request failed — ${e.message}`);
229
+ throw new Error(`${e.message}`);
230
230
  }
231
231
  if (!result.ok) {
232
232
  const errData = result.data;
package/dist/index.js CHANGED
@@ -71,17 +71,23 @@ program
71
71
  .name('solidactions')
72
72
  .description('SolidActions CLI - Deploy and manage workflow automation')
73
73
  .version(pkg.version);
74
- program.option('-w, --workspace <id-or-slug-or-name>', 'override active workspace for this command');
74
+ // Long form is --workspace-override (NOT --workspace) `login` already owns
75
+ // `--workspace` for its own purpose (set the workspace at login time), and
76
+ // commander silently drops a subcommand option whose long name collides with
77
+ // a parent option of the same name, so `login --workspace <x>` was being
78
+ // swallowed by this global flag instead of reaching login()'s own option.
79
+ // The short form `-w` is unaffected and remains the primary way to use this.
80
+ program.option('-w, --workspace-override <id-or-slug-or-name>', 'Override active workspace for this command (short form: -w)');
75
81
  program.hook('preAction', (thisCommand, actionCommand) => {
76
82
  const opts = thisCommand.opts();
77
- const wsOverride = opts.workspace;
83
+ const wsOverride = opts.workspaceOverride;
78
84
  if (wsOverride) {
79
85
  // workspace set is a write — -w is for read-paths only.
80
86
  const fullName = actionCommand.name();
81
87
  const parentName = actionCommand.parent?.name?.();
82
88
  const isWorkspaceSet = fullName === 'set' && parentName === 'workspace';
83
89
  if (isWorkspaceSet) {
84
- console.error(chalk_1.default.yellow('warn:') + ' -w/--workspace is ignored on `workspace set`; the positional argument is the new workspace.');
90
+ console.error(chalk_1.default.yellow('warn:') + ' -w/--workspace-override is ignored on `workspace set`; the positional argument is the new workspace.');
85
91
  return;
86
92
  }
87
93
  (0, config_1.setCliWorkspaceOverride)(wsOverride);
@@ -96,7 +102,7 @@ program
96
102
  .argument('<api-key>', 'Your SolidActions API key')
97
103
  .option('--dev', 'Use local development server (http://localhost:8000)')
98
104
  .option('--host <url>', 'Custom API host URL')
99
- .option('--workspace <name-or-id>', 'Set workspace by name, slug, or ID (skips interactive prompt)')
105
+ .option('--workspace <name-or-id>', 'Set workspace by name, slug, or ID (skips interactive prompt). Non-interactive logins without this flag leave the workspace unset.')
100
106
  .option('--local', 'Save config to ./.solidactions/config.json in the current folder')
101
107
  .option('--global', 'Save config to ~/.solidactions/config.json (default if prompted)')
102
108
  .option('--gitignore', 'With --local, add .solidactions/ to .gitignore without prompting')
@@ -180,15 +186,18 @@ project
180
186
  .command('logs')
181
187
  .description('View build/deployment logs for a project')
182
188
  .argument('<project>', 'Project name (or family name with -e)')
183
- .option('-e, --environment <environment>', 'Environment to resolve (production/staging/dev)')
189
+ .option('-e, --env <environment>', 'Environment to resolve (production/staging/dev)')
190
+ .addOption(new commander_1.Option('--environment <environment>', 'Alias of --env').hideHelp())
184
191
  .action((projectName, options) => {
185
- (0, project_logs_1.logsBuild)(projectName, options.environment);
192
+ const environment = options.env ?? options.environment;
193
+ (0, project_logs_1.logsBuild)(projectName, environment);
186
194
  });
187
195
  project
188
196
  .command('list')
189
197
  .description('List all projects')
190
- .action(() => {
191
- (0, project_list_1.projectList)();
198
+ .option('--json', 'Output as JSON')
199
+ .action((options) => {
200
+ (0, project_list_1.projectList)(options);
192
201
  });
193
202
  // =============================================================================
194
203
  // run <subcommand>
@@ -217,8 +226,10 @@ runCmd
217
226
  .option('--detailed', 'Include timeline, steps, and logs per run (default limit: 5)')
218
227
  .option('--has-errors', 'Show only runs with errors (step errors, retries, or degraded results)')
219
228
  .option('--json', 'Output as JSON')
220
- .option('-e, --environment <environment>', 'Environment to filter by (production/staging/dev)')
229
+ .option('-e, --env <environment>', 'Environment to filter by (production/staging/dev)')
230
+ .addOption(new commander_1.Option('--environment <environment>', 'Alias of --env').hideHelp())
221
231
  .action((projectName, options) => {
232
+ options.environment = options.env ?? options.environment;
222
233
  (0, run_list_1.runs)(projectName, options);
223
234
  });
224
235
  runCmd
@@ -235,10 +246,10 @@ runCmd
235
246
  // =============================================================================
236
247
  // env <subcommand>
237
248
  // =============================================================================
238
- const env = program.command('env').description('Manage environment variables');
249
+ const env = program.command('env').description('Manage variables');
239
250
  env
240
251
  .command('set')
241
- .description('Set an environment variable (create or update, global or project)')
252
+ .description('Set a variable (create or update, global or project)')
242
253
  .argument('<key-or-project>', 'Variable key (global) or project name')
243
254
  .argument('<value-or-key>', 'Variable value (global) or variable key (project)')
244
255
  .argument('[value]', 'Variable value (when first arg is project)')
@@ -256,17 +267,19 @@ env
256
267
  });
257
268
  env
258
269
  .command('list')
259
- .description('List environment variables')
270
+ .description('List variables')
260
271
  .argument('[project]', 'Project name (omit for global variables)')
261
272
  .option('-e, --env <environment>', 'Filter by environment (production/staging/dev)')
273
+ .option('--json', 'Output as JSON')
262
274
  .action((projectName, options) => {
263
275
  (0, env_list_1.envList)(projectName, options);
264
276
  });
265
277
  env
266
278
  .command('delete')
267
- .description('Delete an environment variable')
279
+ .description('Delete a variable')
268
280
  .argument('<key-or-project>', 'Variable key (global) or project name')
269
281
  .argument('[key]', 'Variable key (if first arg is project)')
282
+ .option('-e, --env <environment>', 'Environment to delete from', 'dev')
270
283
  .option('-y, --yes', 'Skip confirmation prompt')
271
284
  .action((keyOrProject, key, options) => {
272
285
  (0, env_delete_1.envDelete)(keyOrProject, key, options);
@@ -283,7 +296,7 @@ env
283
296
  });
284
297
  env
285
298
  .command('pull')
286
- .description('Pull resolved environment variables to a local file')
299
+ .description('Pull resolved variables to a local file')
287
300
  .argument('<project>', 'Project name')
288
301
  .option('-e, --env <environment>', 'Environment (production/staging/dev)', 'dev')
289
302
  .option('-o, --output <file>', 'Output file path (defaults to .env or .env.{environment})')
@@ -294,7 +307,7 @@ env
294
307
  });
295
308
  env
296
309
  .command('push')
297
- .description('Push environment variables from .env file to a project')
310
+ .description('Push variables from .env file to a project')
298
311
  .argument('<project>', 'Project name')
299
312
  .argument('[path]', 'Source directory with solidactions.yaml and .env file', '.')
300
313
  .option('-e, --env <environment>', 'Target environment (production/staging/dev)', 'dev')
@@ -313,7 +326,7 @@ schedule
313
326
  .description('Set a cron schedule for a workflow')
314
327
  .argument('<project>', 'Project name')
315
328
  .argument('<cron>', 'Cron expression (e.g., "0 9 * * *" for daily at 9am)')
316
- .option('-w, --workflow <name>', 'Workflow name (if project has multiple)')
329
+ .option('--workflow <name>', 'Workflow name (if project has multiple)')
317
330
  .option('-i, --input <json>', 'JSON input to pass to scheduled runs')
318
331
  .option('-z, --timezone <iana>', 'IANA timezone the cron is evaluated in (e.g. America/Chicago); defaults to UTC')
319
332
  .option('-y, --yes', 'Skip confirmation if schedule already exists')