@solidactions/cli 1.20.0 → 1.22.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.
@@ -3,11 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GLOBAL_ENV_SCOPE_NOTE = void 0;
7
+ exports.isNonTty = isNonTty;
6
8
  exports.envSet = envSet;
7
9
  const axios_1 = __importDefault(require("axios"));
8
10
  const chalk_1 = __importDefault(require("chalk"));
9
11
  const prompts_1 = __importDefault(require("prompts"));
10
12
  const api_1 = require("../utils/api");
13
+ const env_1 = require("../utils/env");
14
+ /** Returns true when stdin is not an interactive terminal (CI, pipes, scripts). */
15
+ function isNonTty() {
16
+ return !process.stdin.isTTY;
17
+ }
18
+ /** Printed before a 2-arg (global-scope) write — Jordan's runs 3-7 footgun. */
19
+ exports.GLOBAL_ENV_SCOPE_NOTE = [
20
+ 'Note: no project specified — creating a GLOBAL variable.',
21
+ " Global variables are NOT visible to a project's plain `env:` YAML declarations",
22
+ ' unless you map them (`solidactions env map …`).',
23
+ ' For a project variable, use: solidactions env set <project> KEY value',
24
+ ].join('\n');
11
25
  async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
12
26
  const config = await (0, api_1.requireConfigWithWorkspace)();
13
27
  // Detect mode based on arguments
@@ -17,6 +31,10 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
17
31
  const projectName = keyOrProject;
18
32
  const key = valueOrKey;
19
33
  const value = valueIfProject;
34
+ if ((0, env_1.isReservedEnvName)(key)) {
35
+ console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(key)));
36
+ process.exit(1);
37
+ }
20
38
  const environment = options.env || 'dev';
21
39
  // Build project slug
22
40
  const projectSlug = environment === 'production'
@@ -31,6 +49,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
31
49
  const mappings = mappingsResponse.data || [];
32
50
  const existing = mappings.find((m) => m.env_name === key);
33
51
  if (existing && existing.has_value) {
52
+ if (isNonTty()) {
53
+ console.error(chalk_1.default.red(`Variable "${key}" already has a value in "${projectName}" (${environment}). ` +
54
+ `Pass -y / --yes to overwrite without confirmation.`));
55
+ process.exit(1);
56
+ }
34
57
  console.log(chalk_1.default.yellow(`Variable "${key}" already has a value in "${projectName}" (${environment}).`));
35
58
  const confirm = await (0, prompts_1.default)({
36
59
  type: 'confirm',
@@ -83,6 +106,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
83
106
  // Global mode: solidactions env set <key> <value>
84
107
  const key = keyOrProject;
85
108
  const value = valueOrKey;
109
+ if ((0, env_1.isReservedEnvName)(key)) {
110
+ console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(key)));
111
+ process.exit(1);
112
+ }
113
+ console.log(chalk_1.default.yellow(exports.GLOBAL_ENV_SCOPE_NOTE));
86
114
  const isSecret = options.secret || false;
87
115
  // Build the request body with per-environment values
88
116
  const body = {
@@ -119,6 +147,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
119
147
  let action;
120
148
  if (existing) {
121
149
  if (!options.yes) {
150
+ if (isNonTty()) {
151
+ console.error(chalk_1.default.red(`Global variable "${key}" already exists. ` +
152
+ `Pass -y / --yes to overwrite without confirmation.`));
153
+ process.exit(1);
154
+ }
122
155
  console.log(chalk_1.default.yellow(`Global variable "${key}" already exists.`));
123
156
  const confirm = await (0, prompts_1.default)({
124
157
  type: 'confirm',
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TEMPLATE_FILES = void 0;
6
7
  exports.init = init;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -14,8 +15,9 @@ const EXAMPLES_OWNER = 'SolidActions';
14
15
  const EXAMPLES_REPO = 'solidactions-examples';
15
16
  const TEMPLATE_PREFIX = 'templates/minimal';
16
17
  // Tuples of [remote-path-suffix-under-template-prefix, local-path-relative-to-target].
17
- const TEMPLATE_FILES = [
18
+ exports.TEMPLATE_FILES = [
18
19
  ['package.json', 'package.json'],
20
+ ['package-lock.json', 'package-lock.json'],
19
21
  ['tsconfig.json', 'tsconfig.json'],
20
22
  ['solidactions.yaml', 'solidactions.yaml'],
21
23
  ['.env.example', '.env.example'],
@@ -48,7 +50,7 @@ async function init(directory, options = {}) {
48
50
  process.exit(1);
49
51
  }
50
52
  console.log(chalk_1.default.blue(`Scaffolding "${projectName}" in ${targetDir}...`));
51
- for (const [remoteSuffix, localSuffix] of TEMPLATE_FILES) {
53
+ for (const [remoteSuffix, localSuffix] of exports.TEMPLATE_FILES) {
52
54
  const remotePath = `${TEMPLATE_PREFIX}/${remoteSuffix}`;
53
55
  let content = await (0, github_1.fetchRawFile)(EXAMPLES_OWNER, EXAMPLES_REPO, remotePath);
54
56
  content = content.replace(/__PROJECT_NAME__/g, projectName);
@@ -69,9 +71,10 @@ async function init(directory, options = {}) {
69
71
  if (directory) {
70
72
  console.log(chalk_1.default.gray(` cd ${directory}`));
71
73
  }
72
- console.log(chalk_1.default.gray(` # Fill in WEBHOOK_SECRET and any other env vars in solidactions.yaml:`));
73
- console.log(chalk_1.default.gray(` solidactions env set ${projectName} WEBHOOK_SECRET <your-secret> -e production`));
74
74
  console.log(chalk_1.default.gray(` solidactions project deploy ${projectName} -e production`));
75
+ console.log(chalk_1.default.gray(` # If your workflow uses a webhook, retrieve its auto-generated secret`));
76
+ console.log(chalk_1.default.gray(` # and set that value in your sender (e.g. Telegram secret_token):`));
77
+ console.log(chalk_1.default.gray(` solidactions webhook secret ${projectName} -e production`));
75
78
  }
76
79
  catch (err) {
77
80
  if (err.message?.includes('rate limit')) {
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getConfig = getConfig;
7
7
  exports.saveConfig = saveConfig;
8
8
  exports.clearConfig = clearConfig;
9
+ exports.resolveLoginHost = resolveLoginHost;
10
+ exports.loginHostLines = loginHostLines;
9
11
  exports.login = login;
10
12
  exports.logout = logout;
11
13
  exports.whoami = whoami;
@@ -26,17 +28,27 @@ function saveConfig(config) {
26
28
  function clearConfig() {
27
29
  (0, config_1.removeConfigFile)((0, config_1.getGlobalConfigPath)());
28
30
  }
29
- async function login(apiKey, options) {
30
- let host;
31
+ function resolveLoginHost(options) {
31
32
  if (options.host) {
32
- host = options.host;
33
+ return { host: options.host, isDefault: false };
33
34
  }
34
- else if (options.dev) {
35
- host = 'http://localhost:8000';
35
+ if (options.dev) {
36
+ return { host: 'http://localhost:8000', isDefault: false };
36
37
  }
37
- else {
38
- host = 'https://app.solidactions.com';
38
+ return { host: 'https://app.solidactions.com', isDefault: true };
39
+ }
40
+ function loginHostLines(resolved) {
41
+ if (resolved.isDefault) {
42
+ return [
43
+ `Logging into ${resolved.host} (SolidActions Cloud)`,
44
+ ' Self-hosted or local dev? Pass --host <url> (or --dev for http://localhost:8000)',
45
+ ];
39
46
  }
47
+ return [`Host: ${resolved.host}`];
48
+ }
49
+ async function login(apiKey, options) {
50
+ const resolved = resolveLoginHost(options);
51
+ const host = resolved.host;
40
52
  if (!apiKey || apiKey.trim().length === 0) {
41
53
  console.error(chalk_1.default.red('Error: API key is required.'));
42
54
  console.log(chalk_1.default.gray('Generate an API key at: ') + chalk_1.default.blue(`${host}/settings/api-keys`));
@@ -46,7 +58,9 @@ async function login(apiKey, options) {
46
58
  const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global });
47
59
  const targetPath = (0, config_write_target_1.pathForTarget)(target);
48
60
  console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
49
- console.log(chalk_1.default.gray(`Host: ${host}`));
61
+ for (const line of loginHostLines(resolved)) {
62
+ console.log(resolved.isDefault ? chalk_1.default.yellow(line) : chalk_1.default.gray(line));
63
+ }
50
64
  if ((0, config_1.readConfigFile)(targetPath)) {
51
65
  console.log(chalk_1.default.yellow(`Existing config at ${targetPath} will be overwritten.`));
52
66
  }
@@ -3,16 +3,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildBuildLogUrl = buildBuildLogUrl;
7
+ exports.buildBuildLogRequest = buildBuildLogRequest;
6
8
  exports.logsBuild = logsBuild;
7
9
  const axios_1 = __importDefault(require("axios"));
8
10
  const chalk_1 = __importDefault(require("chalk"));
9
11
  const api_1 = require("../utils/api");
10
- async function logsBuild(projectName) {
12
+ function buildBuildLogUrl(host, projectName, environment) {
13
+ if (environment) {
14
+ return `${host}/api/v1/projects/resolve/build-log`;
15
+ }
16
+ return `${host}/api/v1/projects/${projectName}/build-log`;
17
+ }
18
+ function buildBuildLogRequest(host, projectName, environment) {
19
+ if (environment) {
20
+ return {
21
+ url: `${host}/api/v1/projects/resolve/build-log`,
22
+ params: { name: projectName, environment },
23
+ };
24
+ }
25
+ return { url: `${host}/api/v1/projects/${projectName}/build-log` };
26
+ }
27
+ async function logsBuild(projectName, environment) {
11
28
  const config = await (0, api_1.requireConfigWithWorkspace)();
12
29
  console.log(chalk_1.default.blue(`Fetching build logs for project "${projectName}"...`));
30
+ const { url, params } = buildBuildLogRequest(config.host, projectName, environment);
13
31
  try {
14
- const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectName}/build-log`, {
32
+ const response = await axios_1.default.get(url, {
15
33
  headers: (0, api_1.getApiHeaders)(config),
34
+ params,
16
35
  });
17
36
  const buildLog = response.data.build_log || response.data;
18
37
  if (!buildLog || buildLog.length === 0) {
@@ -29,7 +48,11 @@ async function logsBuild(projectName) {
29
48
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
30
49
  }
31
50
  else if (error.response.status === 404) {
32
- console.error(chalk_1.default.red(`Project "${projectName}" not found.`));
51
+ console.error(chalk_1.default.red(error.response.data?.message ?? `Project "${projectName}" not found.`));
52
+ const envs = error.response.data?.available_environments;
53
+ if (envs && envs.length > 0) {
54
+ console.error(chalk_1.default.yellow(`Available environments: ${envs.join(', ')}. Pass -e <env> to select one.`));
55
+ }
33
56
  }
34
57
  else {
35
58
  console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
@@ -3,38 +3,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildRunListParams = buildRunListParams;
6
7
  exports.runs = runs;
7
8
  exports.summaryStatusLabel = summaryStatusLabel;
8
9
  exports.detailedOutcomeTag = detailedOutcomeTag;
9
10
  const axios_1 = __importDefault(require("axios"));
10
11
  const chalk_1 = __importDefault(require("chalk"));
11
12
  const api_1 = require("../utils/api");
12
- async function runs(projectName, options = {}) {
13
- const config = await (0, api_1.requireConfigWithWorkspace)();
14
- // Default limit is lower for --detailed (more data per run)
13
+ /**
14
+ * Build the params object for GET /api/v1/runs.
15
+ * Extracted as a pure function so it can be unit-tested without mocking axios.
16
+ */
17
+ function buildRunListParams(projectName, options = {}) {
15
18
  const defaultLimit = options.detailed ? 5 : 20;
16
19
  const limit = options.limit || defaultLimit;
20
+ const params = { limit };
21
+ if (projectName)
22
+ params.project = projectName;
23
+ if (options.environment)
24
+ params.environment = options.environment;
25
+ if (options.offset)
26
+ params.offset = options.offset;
27
+ if (options.status)
28
+ params.status = options.status;
29
+ if (options.since)
30
+ params.since = parseSince(options.since);
31
+ if (options.workflow)
32
+ params.workflow = options.workflow;
33
+ if (options.detailed)
34
+ params.detailed = '1';
35
+ if (options.hasErrors)
36
+ params.has_errors = '1';
37
+ return params;
38
+ }
39
+ async function runs(projectName, options = {}) {
40
+ const config = await (0, api_1.requireConfigWithWorkspace)();
17
41
  try {
18
- const params = { limit };
19
- if (projectName)
20
- params.project = projectName;
21
- if (options.offset)
22
- params.offset = options.offset;
23
- if (options.status)
24
- params.status = options.status;
25
- if (options.since)
26
- params.since = parseSince(options.since);
27
- if (options.workflow)
28
- params.workflow = options.workflow;
29
- if (options.detailed)
30
- params.detailed = '1';
31
- if (options.hasErrors)
32
- params.has_errors = '1';
42
+ const params = buildRunListParams(projectName, options);
33
43
  const response = await axios_1.default.get(`${config.host}/api/v1/runs`, {
34
44
  headers: (0, api_1.getApiHeaders)(config),
35
45
  params,
36
46
  });
37
47
  const runsList = response.data.data || response.data;
48
+ // Check server-side error fields FIRST (project_not_found comes back as 200 with data:[])
49
+ const serverError = response.data.error;
50
+ if (serverError === 'project_not_found') {
51
+ console.error(chalk_1.default.red(response.data.message || `Project '${projectName}' not found.`));
52
+ process.exit(1);
53
+ }
38
54
  if (!runsList || runsList.length === 0) {
39
55
  if (options.json) {
40
56
  console.log('[]');
@@ -58,7 +74,13 @@ async function runs(projectName, options = {}) {
58
74
  }
59
75
  catch (error) {
60
76
  if (error.response) {
61
- if (error.response.status === 401) {
77
+ // ambiguous_project is 422 — axios throws, so handle here
78
+ if (error.response.status === 422 && error.response.data?.error === 'ambiguous_project') {
79
+ const envs = (error.response.data.environments ?? []).join(', ');
80
+ console.error(chalk_1.default.yellow(`Ambiguous project '${projectName}': found in environments: ${envs}.`));
81
+ console.error(chalk_1.default.yellow(`Pass -e <env> to disambiguate, e.g.: solidactions run list ${projectName} -e dev`));
82
+ }
83
+ else if (error.response.status === 401) {
62
84
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
63
85
  }
64
86
  else {
@@ -4,6 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runView = runView;
7
+ exports.displayStepsTable = displayStepsTable;
8
+ exports.flattenSteps = flattenSteps;
9
+ exports.stepDisplayStatus = stepDisplayStatus;
10
+ exports.errorMessage = errorMessage;
7
11
  const axios_1 = __importDefault(require("axios"));
8
12
  const chalk_1 = __importDefault(require("chalk"));
9
13
  const api_1 = require("../utils/api");
@@ -149,7 +153,7 @@ function displayFullView(runData, timeline, steps, logsData) {
149
153
  if (runData.error) {
150
154
  console.log('');
151
155
  console.log(chalk_1.default.bold.red(' Error:'));
152
- console.log(chalk_1.default.red(` ${runData.error}`));
156
+ console.log(chalk_1.default.red(` ${errorMessage(runData.error)}`));
153
157
  }
154
158
  // Logs snippet
155
159
  const logs = logsData.logs || '';
@@ -203,11 +207,22 @@ function displayStepsTable(steps, indent = ' ') {
203
207
  console.log(chalk_1.default.gray(`${indent}${'─'.repeat(72)}`));
204
208
  for (const step of steps) {
205
209
  const name = (step.name || '?').padEnd(24);
206
- const status = step.completedAt ? 'completed' : step.startedAt ? 'running' : 'pending';
210
+ const status = stepDisplayStatus(step);
207
211
  const statusColor = getStatusColor(status);
208
212
  const duration = formatDuration(step.durationMs);
209
- const output = step.output ? truncate(JSON.stringify(unwrapOutput(step.output)), 40) : '-';
210
- console.log(`${indent}${name}${statusColor(status.padEnd(12))}${chalk_1.default.gray(duration.padEnd(12))}${chalk_1.default.gray(output)}`);
213
+ // A failed step's error is more useful than its (null) output show it, red.
214
+ const output = step.error
215
+ ? chalk_1.default.red(truncate(errorMessage(step.error).replace(/\s+/g, ' ').trim(), 40))
216
+ : chalk_1.default.gray(step.output ? truncate(JSON.stringify(unwrapOutput(step.output)), 40) : '-');
217
+ console.log(`${indent}${name}${statusColor(status.padEnd(12))}${chalk_1.default.gray(duration.padEnd(12))}${output}`);
218
+ }
219
+ // Untruncated first failure below the table: `run view` alone must be
220
+ // enough to diagnose (Wall #5 — failed steps used to render "completed").
221
+ const firstFailed = steps.find((s) => stepDisplayStatus(s).toLowerCase() === 'failed' && s.error);
222
+ if (firstFailed) {
223
+ console.log('');
224
+ console.log(chalk_1.default.bold.red(`${indent}Step "${firstFailed.name}" failed:`));
225
+ console.log(chalk_1.default.red(`${indent} ${errorMessage(firstFailed.error)}`));
211
226
  }
212
227
  }
213
228
  // ─── Utility ───────────────────────────────────────────────────────────────
@@ -221,17 +236,52 @@ function flattenSteps(workers) {
221
236
  completedAt: step.completed_at || null,
222
237
  durationMs: step.duration_ms ?? null,
223
238
  output: step.output ?? null,
239
+ status: step.status ?? null,
240
+ error: step.error ?? null,
224
241
  });
225
242
  }
226
243
  }
227
244
  return steps;
228
245
  }
246
+ /** Server-derived status wins; timestamp heuristic only for pre-status API responses. */
247
+ function stepDisplayStatus(step) {
248
+ return step.status ?? (step.completedAt ? 'completed' : step.startedAt ? 'running' : 'pending');
249
+ }
229
250
  function unwrapOutput(output) {
230
251
  if (output && output.__solidactions_serializer === 'superjson' && output.json) {
231
252
  return output.json;
232
253
  }
233
254
  return output;
234
255
  }
256
+ /**
257
+ * Normalize a step/run `error` field to a readable string. Errors arrive in
258
+ * one of three shapes: a plain string (legacy servers), a superjson-wrapped
259
+ * `{ json: { name, message, stack }, __solidactions_serializer: 'superjson' }`
260
+ * object (steps endpoint), or that same shape JSON.stringified into a string
261
+ * (run-level `error` field) — confirmed live via GET /api/v1/runs/{id} and
262
+ * .../steps. Without this, a thrown Error renders as "[object Object]".
263
+ */
264
+ function errorMessage(error) {
265
+ if (error === null || error === undefined)
266
+ return '';
267
+ let value = error;
268
+ if (typeof value === 'string') {
269
+ try {
270
+ value = JSON.parse(value);
271
+ }
272
+ catch {
273
+ return value;
274
+ }
275
+ }
276
+ if (value && typeof value === 'object' && value.__solidactions_serializer === 'superjson' && value.json) {
277
+ const inner = value.json;
278
+ if (inner && typeof inner === 'object' && (inner.name || inner.message)) {
279
+ return inner.name && inner.message ? `${inner.name}: ${inner.message}` : String(inner.name || inner.message);
280
+ }
281
+ return JSON.stringify(inner);
282
+ }
283
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
284
+ }
235
285
  function formatDuration(ms) {
236
286
  if (ms === null || ms === undefined)
237
287
  return '-';
@@ -20,18 +20,20 @@ async function scheduleList(projectName) {
20
20
  return;
21
21
  }
22
22
  console.log('');
23
- console.log(chalk_1.default.gray('ID'.padEnd(8) + 'WORKFLOW'.padEnd(25) + 'CRON'.padEnd(18) + 'ENABLED'.padEnd(10) + 'NEXT RUN'));
24
- console.log(chalk_1.default.gray('-'.repeat(95)));
23
+ console.log(chalk_1.default.gray('ID'.padEnd(8) + 'WORKFLOW'.padEnd(25) + 'CRON'.padEnd(18) + 'TIMEZONE'.padEnd(20) + 'ENABLED'.padEnd(10) + 'NEXT RUN'));
24
+ console.log(chalk_1.default.gray('-'.repeat(105)));
25
25
  for (const schedule of schedules) {
26
26
  const id = schedule.id?.toString() || '?';
27
27
  const workflow = schedule.workflow_name || schedule.workflow_slug || '?';
28
28
  const cron = schedule.cron_expression || '?';
29
+ const timezone = schedule.timezone || 'UTC';
29
30
  const enabled = schedule.enabled;
30
31
  const nextRun = schedule.next_run_at ? formatRelativeTime(schedule.next_run_at) : '-';
31
32
  const enabledColor = enabled ? chalk_1.default.green : chalk_1.default.red;
32
33
  console.log(chalk_1.default.gray(id.padEnd(8)) +
33
34
  workflow.padEnd(25) +
34
35
  chalk_1.default.cyan(cron.padEnd(18)) +
36
+ chalk_1.default.gray(timezone.padEnd(20)) +
35
37
  enabledColor((enabled ? 'yes' : 'no').padEnd(10)) +
36
38
  chalk_1.default.gray(nextRun));
37
39
  }
@@ -3,11 +3,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildSchedulePayload = buildSchedulePayload;
7
+ exports.timezoneMismatch = timezoneMismatch;
8
+ exports.timezoneMismatchRemedy = timezoneMismatchRemedy;
6
9
  exports.scheduleSet = scheduleSet;
7
10
  const axios_1 = __importDefault(require("axios"));
8
11
  const chalk_1 = __importDefault(require("chalk"));
9
12
  const prompts_1 = __importDefault(require("prompts"));
10
13
  const api_1 = require("../utils/api");
14
+ function buildSchedulePayload(cron, options, inputData) {
15
+ const payload = { cron };
16
+ if (options.workflow) {
17
+ payload.workflow = options.workflow;
18
+ }
19
+ if (inputData) {
20
+ payload.input = inputData;
21
+ }
22
+ if (options.timezone) {
23
+ payload.timezone = options.timezone;
24
+ }
25
+ return payload;
26
+ }
27
+ /** True when the user asked for a timezone the server did not apply (pre-item-5-App server). */
28
+ function timezoneMismatch(requested, returned) {
29
+ return requested !== undefined && returned !== requested;
30
+ }
31
+ /**
32
+ * Remediation text for a timezoneMismatch. `schedule delete` takes a numeric
33
+ * <schedule-id>, not a workflow name — point at `schedule list` first to find it.
34
+ */
35
+ function timezoneMismatchRemedy(projectName) {
36
+ return `Your server may not support schedule timezones yet; update the server, or run 'solidactions schedule list ${projectName}' to find the schedule ID and remove it with: solidactions schedule delete ${projectName} <schedule-id>`;
37
+ }
11
38
  async function scheduleSet(projectName, cron, options) {
12
39
  const config = await (0, api_1.requireConfigWithWorkspace)();
13
40
  // Parse input JSON if provided
@@ -66,20 +93,25 @@ async function scheduleSet(projectName, cron, options) {
66
93
  }
67
94
  console.log(chalk_1.default.blue(`Setting schedule for project "${projectName}"...`));
68
95
  try {
69
- const payload = {
70
- cron,
71
- };
72
- if (options.workflow) {
73
- payload.workflow = options.workflow;
74
- }
75
- if (inputData) {
76
- payload.input = inputData;
77
- }
78
- await axios_1.default.post(`${config.host}/api/v1/projects/${projectName}/schedules`, payload, {
96
+ const payload = buildSchedulePayload(cron, options, inputData);
97
+ const response = await axios_1.default.post(`${config.host}/api/v1/projects/${projectName}/schedules`, payload, {
79
98
  headers: (0, api_1.getApiHeaders)(config, 'application/json'),
80
99
  });
100
+ // Verify, don't trust: a server predating timezone support silently
101
+ // strips the field, but the schedule is already persisted by this point —
102
+ // it's live and running in the wrong timezone. Report the persisted fact
103
+ // and the remedy, not a hypothetical.
104
+ const returnedTz = response.data?.schedule?.timezone;
105
+ if (timezoneMismatch(options.timezone, returnedTz)) {
106
+ console.error(chalk_1.default.red(`A schedule was created but is running in ${returnedTz ?? 'UTC'} — not ${options.timezone} as requested.`));
107
+ console.error(chalk_1.default.red(timezoneMismatchRemedy(projectName)));
108
+ process.exit(1);
109
+ }
81
110
  console.log(chalk_1.default.green(`Schedule set successfully!`));
82
111
  console.log(chalk_1.default.gray(` Cron: ${cron}`));
112
+ if (options.timezone) {
113
+ console.log(chalk_1.default.gray(` Timezone: ${options.timezone}`));
114
+ }
83
115
  if (options.workflow) {
84
116
  console.log(chalk_1.default.gray(` Workflow: ${options.workflow}`));
85
117
  }
@@ -0,0 +1,74 @@
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.formatSecretOutput = formatSecretOutput;
7
+ exports.webhookSecret = webhookSecret;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const api_1 = require("../utils/api");
11
+ function formatSecretOutput(rows, workflowFilter) {
12
+ if (workflowFilter) {
13
+ const match = rows.find((r) => r.workflow_name === workflowFilter);
14
+ if (!match)
15
+ return `No webhook named "${workflowFilter}" found.`;
16
+ return match.webhook_secret ?? '(secret not returned — check your access level)';
17
+ }
18
+ if (rows.length === 1) {
19
+ return rows[0].webhook_secret ?? '(secret not returned — check your access level)';
20
+ }
21
+ const lines = rows.map((r) => `${(r.workflow_name ?? '?').padEnd(32)}${r.webhook_secret ?? '(none)'}`);
22
+ return ['WORKFLOW'.padEnd(32) + 'SECRET', '-'.repeat(80), ...lines].join('\n');
23
+ }
24
+ async function webhookSecret(projectName, options = {}) {
25
+ const format = options.format ?? 'text';
26
+ if (format !== 'text' && format !== 'json') {
27
+ console.error(chalk_1.default.red(`Invalid --format: ${format}. Expected 'text' or 'json'.`));
28
+ process.exit(1);
29
+ }
30
+ const config = await (0, api_1.requireConfigWithWorkspace)();
31
+ const environment = options.env || 'dev';
32
+ const projectSlug = environment === 'production' ? projectName : `${projectName}-${environment}`;
33
+ if (format === 'text') {
34
+ process.stderr.write(chalk_1.default.dim(`(environment: ${environment} — pass -e <env> to change)\n`));
35
+ }
36
+ try {
37
+ const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/webhooks`, {
38
+ headers: (0, api_1.getApiHeaders)(config),
39
+ params: { show_secrets: 'true' },
40
+ });
41
+ const rows = response.data.data ?? [];
42
+ if (rows.length === 0) {
43
+ console.error(chalk_1.default.yellow(`No webhook workflows found for project "${projectName}"${environment !== 'production' ? ` (${environment})` : ''}.`));
44
+ process.exit(1);
45
+ }
46
+ if (format === 'json') {
47
+ const out = options.workflow
48
+ ? rows
49
+ .filter((r) => r.workflow_name === options.workflow)
50
+ .map((r) => ({ workflow: r.workflow_name, secret: r.webhook_secret }))
51
+ : rows.map((r) => ({ workflow: r.workflow_name, secret: r.webhook_secret }));
52
+ console.log(JSON.stringify(out, null, 2));
53
+ return;
54
+ }
55
+ console.log(formatSecretOutput(rows, options.workflow));
56
+ }
57
+ catch (error) {
58
+ if (error.response) {
59
+ if (error.response.status === 401) {
60
+ console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
61
+ }
62
+ else if (error.response.status === 404) {
63
+ console.error(chalk_1.default.red(`Project "${projectName}" not found.`));
64
+ }
65
+ else {
66
+ console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
67
+ }
68
+ }
69
+ else {
70
+ console.error(chalk_1.default.red('Connection failed:'), error.message);
71
+ }
72
+ process.exit(1);
73
+ }
74
+ }
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ const schedule_set_1 = require("./commands/schedule-set");
26
26
  const schedule_list_1 = require("./commands/schedule-list");
27
27
  const schedule_delete_1 = require("./commands/schedule-delete");
28
28
  const webhook_list_1 = require("./commands/webhook-list");
29
+ const webhook_secret_1 = require("./commands/webhook-secret");
29
30
  const dev_1 = require("./commands/dev");
30
31
  const ai_init_1 = require("./commands/ai-init");
31
32
  const ai_examples_1 = require("./commands/ai-examples");
@@ -158,8 +159,13 @@ project
158
159
  .option('-e, --env <environment>', 'Target environment (production/staging/dev). Required on first deploy of a new project.')
159
160
  .option('--create', 'Create environment project if it doesn\'t exist')
160
161
  .option('--config-only', 'Sync YAML env declarations without building/deploying')
162
+ .option('--no-cache', 'Force a fresh build, bypassing all build caches')
163
+ .option('--force-rebuild', 'Force a fresh build, bypassing all build caches (alias for --no-cache)')
161
164
  .action((projectName, path, options) => {
162
- (0, deploy_1.deploy)(projectName, path, options);
165
+ // Commander's negation convention maps --no-cache to options.cache === false
166
+ // (NOT options.noCache). Normalize both flags to a single boolean.
167
+ const noCache = options.cache === false || options.forceRebuild === true;
168
+ (0, deploy_1.deploy)(projectName, path, { ...options, noCache });
163
169
  });
164
170
  project
165
171
  .command('pull')
@@ -173,9 +179,10 @@ project
173
179
  project
174
180
  .command('logs')
175
181
  .description('View build/deployment logs for a project')
176
- .argument('<project>', 'Project name')
177
- .action((projectName) => {
178
- (0, project_logs_1.logsBuild)(projectName);
182
+ .argument('<project>', 'Project name (or family name with -e)')
183
+ .option('-e, --environment <environment>', 'Environment to resolve (production/staging/dev)')
184
+ .action((projectName, options) => {
185
+ (0, project_logs_1.logsBuild)(projectName, options.environment);
179
186
  });
180
187
  project
181
188
  .command('list')
@@ -210,6 +217,7 @@ runCmd
210
217
  .option('--detailed', 'Include timeline, steps, and logs per run (default limit: 5)')
211
218
  .option('--has-errors', 'Show only runs with errors (step errors, retries, or degraded results)')
212
219
  .option('--json', 'Output as JSON')
220
+ .option('-e, --environment <environment>', 'Environment to filter by (production/staging/dev)')
213
221
  .action((projectName, options) => {
214
222
  (0, run_list_1.runs)(projectName, options);
215
223
  });
@@ -306,6 +314,7 @@ schedule
306
314
  .argument('<cron>', 'Cron expression (e.g., "0 9 * * *" for daily at 9am)')
307
315
  .option('-w, --workflow <name>', 'Workflow name (if project has multiple)')
308
316
  .option('-i, --input <json>', 'JSON input to pass to scheduled runs')
317
+ .option('-z, --timezone <iana>', 'IANA timezone the cron is evaluated in (e.g. America/Chicago); defaults to UTC')
309
318
  .option('-y, --yes', 'Skip confirmation if schedule already exists')
310
319
  .action((projectName, cron, options) => {
311
320
  (0, schedule_set_1.scheduleSet)(projectName, cron, options);
@@ -340,6 +349,16 @@ webhook
340
349
  .action((projectName, options) => {
341
350
  (0, webhook_list_1.webhookList)(projectName, options);
342
351
  });
352
+ webhook
353
+ .command('secret')
354
+ .description('Print the webhook secret for a project (set this value in your sender)')
355
+ .argument('<project>', 'Project name')
356
+ .option('-e, --env <environment>', 'Environment (production/staging/dev)')
357
+ .option('--workflow <name>', 'Filter to a specific workflow by name')
358
+ .option('--format <format>', 'Output format: text or json', 'text')
359
+ .action((projectName, options) => {
360
+ (0, webhook_secret_1.webhookSecret)(projectName, options);
361
+ });
343
362
  // =============================================================================
344
363
  // workspace <subcommand>
345
364
  // =============================================================================