@solidactions/cli 1.21.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,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.SKILLS_TIP_LINES = void 0;
6
7
  exports.cacheBusterEntry = cacheBusterEntry;
7
8
  exports.shouldPrintWorkspaceMismatch = shouldPrintWorkspaceMismatch;
8
9
  exports.deploy = deploy;
@@ -19,6 +20,13 @@ const env_1 = require("../utils/env");
19
20
  const api_1 = require("../utils/api");
20
21
  const deploy_ignore_1 = require("../utils/deploy-ignore");
21
22
  const slug_1 = require("../utils/slug");
23
+ const skills_1 = require("../utils/skills");
24
+ /** Printed when a deploy target has no SolidActions skill files installed. */
25
+ exports.SKILLS_TIP_LINES = [
26
+ 'Tip: no SolidActions skill files found (.claude/skills/solidactions-*). Your AI assistant',
27
+ 'works much better with them — run `solidactions ai init --claude` (or --agents) in this',
28
+ 'directory to add them.',
29
+ ];
22
30
  /**
23
31
  * Validate project structure before deployment.
24
32
  * Checks for required files and SDK dependency.
@@ -162,6 +170,13 @@ async function deploy(projectName, sourcePath, options = {}) {
162
170
  console.error(chalk_1.default.red(`Source directory not found: ${sourceDir}`));
163
171
  process.exit(1);
164
172
  }
173
+ // Non-blocking: AI assistants work measurably better with the skill files
174
+ // (field report: a 600-line skill file cracked the env-scope bug).
175
+ if (!(0, skills_1.hasSolidActionsSkills)(sourceDir)) {
176
+ for (const line of exports.SKILLS_TIP_LINES) {
177
+ console.log(chalk_1.default.yellow(line));
178
+ }
179
+ }
165
180
  // -------------------------------------------------------------------------
166
181
  // First-deploy check — must happen BEFORE we apply any env default so that
167
182
  // first-time deploys without -e get a helpful error prompting a deliberate
@@ -8,8 +8,13 @@ const axios_1 = __importDefault(require("axios"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const prompts_1 = __importDefault(require("prompts"));
10
10
  const api_1 = require("../utils/api");
11
+ const env_1 = require("../utils/env");
11
12
  async function envMap(projectName, projectKey, globalKey, options = {}) {
12
13
  const config = await (0, api_1.requireConfigWithWorkspace)();
14
+ if ((0, env_1.isReservedEnvName)(projectKey)) {
15
+ console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(projectKey)));
16
+ process.exit(1);
17
+ }
13
18
  console.log(chalk_1.default.blue(`Mapping global variable "${globalKey}" to project key "${projectKey}" in "${projectName}"...`));
14
19
  try {
15
20
  // First, get the global variable ID by key
@@ -72,6 +72,15 @@ async function envPush(projectName, sourcePath, options = {}) {
72
72
  }
73
73
  process.exit(0);
74
74
  }
75
+ // Hard-reject reserved names among the keys that would actually be pushed
76
+ // (the server rejects them too — fail here, before any network call).
77
+ const reservedKeys = keysToProcess.filter(env_1.isReservedEnvName);
78
+ if (reservedKeys.length > 0) {
79
+ console.error(chalk_1.default.red(`Refusing to push — reserved ${env_1.RESERVED_ENV_PREFIX} names found: ${reservedKeys.join(', ')}`));
80
+ console.error(chalk_1.default.red('These names are set by the platform at runtime (API credentials, run context) and a custom variable would clobber them, causing authentication failures. ' +
81
+ `Rename them (e.g. "${reservedKeys[0].replace(/^SOLIDACTIONS_/, 'MY_')}") and retry. Nothing was pushed.`));
82
+ process.exit(1);
83
+ }
75
84
  // Build project slug
76
85
  const projectSlug = environment === 'production'
77
86
  ? projectName
@@ -3,16 +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;
6
7
  exports.isNonTty = isNonTty;
7
8
  exports.envSet = envSet;
8
9
  const axios_1 = __importDefault(require("axios"));
9
10
  const chalk_1 = __importDefault(require("chalk"));
10
11
  const prompts_1 = __importDefault(require("prompts"));
11
12
  const api_1 = require("../utils/api");
13
+ const env_1 = require("../utils/env");
12
14
  /** Returns true when stdin is not an interactive terminal (CI, pipes, scripts). */
13
15
  function isNonTty() {
14
16
  return !process.stdin.isTTY;
15
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');
16
25
  async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
17
26
  const config = await (0, api_1.requireConfigWithWorkspace)();
18
27
  // Detect mode based on arguments
@@ -22,6 +31,10 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
22
31
  const projectName = keyOrProject;
23
32
  const key = valueOrKey;
24
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
+ }
25
38
  const environment = options.env || 'dev';
26
39
  // Build project slug
27
40
  const projectSlug = environment === 'production'
@@ -93,6 +106,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
93
106
  // Global mode: solidactions env set <key> <value>
94
107
  const key = keyOrProject;
95
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));
96
114
  const isSecret = options.secret || false;
97
115
  // Build the request body with per-environment values
98
116
  const body = {
@@ -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);
@@ -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
  }
@@ -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
  }
package/dist/index.js CHANGED
@@ -314,6 +314,7 @@ schedule
314
314
  .argument('<cron>', 'Cron expression (e.g., "0 9 * * *" for daily at 9am)')
315
315
  .option('-w, --workflow <name>', 'Workflow name (if project has multiple)')
316
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')
317
318
  .option('-y, --yes', 'Skip confirmation if schedule already exists')
318
319
  .action((projectName, cron, options) => {
319
320
  (0, schedule_set_1.scheduleSet)(projectName, cron, options);
package/dist/utils/env.js CHANGED
@@ -3,10 +3,13 @@ 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.RESERVED_ENV_PREFIX = void 0;
6
7
  exports.parseEnvFile = parseEnvFile;
7
8
  exports.parseYamlEnvVars = parseYamlEnvVars;
8
9
  exports.getYamlDeclaredVars = getYamlDeclaredVars;
9
10
  exports.loadSolidActionsConfig = loadSolidActionsConfig;
11
+ exports.isReservedEnvName = isReservedEnvName;
12
+ exports.reservedEnvNameError = reservedEnvNameError;
10
13
  const fs_1 = __importDefault(require("fs"));
11
14
  const js_yaml_1 = __importDefault(require("js-yaml"));
12
15
  /**
@@ -91,3 +94,19 @@ function loadSolidActionsConfig(yamlPath) {
91
94
  const content = fs_1.default.readFileSync(yamlPath, 'utf8');
92
95
  return js_yaml_1.default.load(content);
93
96
  }
97
+ /**
98
+ * Reserved runtime env-name prefix. SOLIDACTIONS_* names are set by the
99
+ * platform at dispatch time (SOLIDACTIONS_API_KEY, SOLIDACTIONS_API_URL, run
100
+ * context); a custom variable with such a name clobbers the platform
101
+ * credential — the field failure mode is a bare 401 out of
102
+ * InvokeSystemDatabase.init before any workflow code runs. Case-sensitive,
103
+ * matching the server-side rule (App\Support\ReservedEnvNames).
104
+ */
105
+ exports.RESERVED_ENV_PREFIX = 'SOLIDACTIONS_';
106
+ function isReservedEnvName(key) {
107
+ return key.startsWith(exports.RESERVED_ENV_PREFIX);
108
+ }
109
+ function reservedEnvNameError(key) {
110
+ const suggestion = key.replace(/^SOLIDACTIONS_/, 'MY_');
111
+ return `"${key}" uses the reserved ${exports.RESERVED_ENV_PREFIX} prefix. These names are set by the platform at runtime (API credentials, run context) and a custom variable would clobber them, causing authentication failures. Choose a different name (e.g. "${suggestion}").`;
112
+ }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.skillTargetDir = skillTargetDir;
7
7
  exports.installSkills = installSkills;
8
8
  exports.fetchAiHelperContent = fetchAiHelperContent;
9
+ exports.hasSolidActionsSkills = hasSolidActionsSkills;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -58,3 +59,21 @@ async function installSkills(targetDir) {
58
59
  async function fetchAiHelperContent(targetFile) {
59
60
  return (0, github_1.fetchRawFile)(EXAMPLES_OWNER, EXAMPLES_REPO, targetFile);
60
61
  }
62
+ /**
63
+ * True when the project dir already has SolidActions skill files installed
64
+ * (either AI-helper convention). Used by `deploy` for a non-blocking tip —
65
+ * these files are how AI assistants self-rescue on env-scope/deploy traps.
66
+ */
67
+ function hasSolidActionsSkills(projectDir) {
68
+ for (const target of ['CLAUDE.md', 'AGENTS.md']) {
69
+ const dir = skillTargetDir(target, projectDir);
70
+ if (!fs_1.default.existsSync(dir)) {
71
+ continue;
72
+ }
73
+ const entries = fs_1.default.readdirSync(dir);
74
+ if (entries.some((f) => f.startsWith('solidactions-') && f.endsWith('.md'))) {
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {