@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.
package/README.md CHANGED
@@ -44,6 +44,8 @@ You can mix: e.g., set only `SOLIDACTIONS_WORKSPACE_ID` in the environment while
44
44
 
45
45
  In interactive shells, `login` without `--local`/`--global` prompts for a location. In non-interactive contexts, one of the flags is required.
46
46
 
47
+ If the target config file already exists and its contents would change, `login` writes a timestamped backup (e.g. `config.json.bak-2026-07-05T12-30-00Z`) alongside it before overwriting, and prints the backup path. Non-interactive runs proceed automatically (with the backup); an interactive TTY additionally asks a y/N confirmation first.
48
+
47
49
  ### `solidactions logout` flags
48
50
 
49
51
  - `--local` — remove only the nearest local config (walks up from cwd).
@@ -3,6 +3,8 @@ 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.NON_TTY_DEFAULT_NOTICE = void 0;
7
+ exports.resolveAiHelperTarget = resolveAiHelperTarget;
6
8
  exports.aiInit = aiInit;
7
9
  const fs_1 = __importDefault(require("fs"));
8
10
  const chalk_1 = __importDefault(require("chalk"));
@@ -12,36 +14,49 @@ const path_1 = __importDefault(require("path"));
12
14
  const github_1 = require("../utils/github");
13
15
  const markers_1 = require("../utils/markers");
14
16
  const skills_1 = require("../utils/skills");
15
- async function aiInit(options = {}) {
16
- try {
17
- // Determine target file.
18
- let targetFile;
19
- if (options.claude && options.agents) {
20
- console.error(chalk_1.default.red('Please specify only one of --claude or --agents'));
21
- process.exit(1);
22
- }
23
- if (options.claude) {
24
- targetFile = 'CLAUDE.md';
25
- }
26
- else if (options.agents) {
27
- targetFile = 'AGENTS.md';
28
- }
29
- else {
30
- const response = await (0, prompts_1.default)({
31
- type: 'select',
32
- name: 'file',
33
- message: 'Which AI helper file?',
34
- choices: [
35
- { title: 'CLAUDE.md (Claude Code)', value: 'CLAUDE.md' },
36
- { title: 'AGENTS.md (Codex, Cursor, Gemini, etc.)', value: 'AGENTS.md' },
37
- ],
38
- });
39
- if (!response.file) {
40
- console.log(chalk_1.default.yellow('Cancelled.'));
41
- process.exit(0);
42
- }
43
- targetFile = response.file;
17
+ exports.NON_TTY_DEFAULT_NOTICE = 'No --claude/--agents given and non-interactive — defaulting to CLAUDE.md; pass --agents for Codex/Cursor/Gemini.';
18
+ /**
19
+ * Resolve which AI-helper file (CLAUDE.md vs AGENTS.md) to target, WITHOUT
20
+ * touching the filesystem or network. Callers that also scaffold template
21
+ * files (e.g. `init`) must call this BEFORE writing anything, so a shell
22
+ * that can't answer the interactive prompt never leaves a half-scaffolded
23
+ * project — see the `init` half-scaffold bug this closes.
24
+ */
25
+ async function resolveAiHelperTarget(options = {}) {
26
+ if (options.claude && options.agents) {
27
+ console.error(chalk_1.default.red('Please specify only one of --claude or --agents'));
28
+ process.exit(1);
29
+ }
30
+ if (options.claude) {
31
+ return 'CLAUDE.md';
32
+ }
33
+ if (options.agents) {
34
+ return 'AGENTS.md';
35
+ }
36
+ if (process.stdin.isTTY) {
37
+ const response = await (0, prompts_1.default)({
38
+ type: 'select',
39
+ name: 'file',
40
+ message: 'Which AI helper file?',
41
+ choices: [
42
+ { title: 'CLAUDE.md (Claude Code)', value: 'CLAUDE.md' },
43
+ { title: 'AGENTS.md (Codex, Cursor, Gemini, etc.)', value: 'AGENTS.md' },
44
+ ],
45
+ });
46
+ if (!response.file) {
47
+ console.log(chalk_1.default.yellow('Cancelled.'));
48
+ process.exit(0);
44
49
  }
50
+ return response.file;
51
+ }
52
+ // Non-TTY (CI, pipes, an AI agent's non-interactive shell) with no flag:
53
+ // never block on an unanswerable prompt — default instead.
54
+ console.log(chalk_1.default.yellow(exports.NON_TTY_DEFAULT_NOTICE));
55
+ return 'CLAUDE.md';
56
+ }
57
+ async function aiInit(options = {}, resolvedTarget) {
58
+ try {
59
+ const targetFile = resolvedTarget ?? await resolveAiHelperTarget(options);
45
60
  console.log(chalk_1.default.blue('Fetching AI helper content...'));
46
61
  // Fetch slim helper content for the chosen target.
47
62
  const aiContent = await (0, skills_1.fetchAiHelperContent)(targetFile);
@@ -400,7 +400,7 @@ async function deploy(projectName, sourcePath, options = {}) {
400
400
  if (status === 'deployed') {
401
401
  clearInterval(poll);
402
402
  console.log(chalk_1.default.green(`\n✓ Deployed to ${projectSlug}${envLabel}!`));
403
- // Always sync YAML declarations (registers env vars and their mappings)
403
+ // Always sync YAML declarations (registers variables and their mappings)
404
404
  if (yamlConfig) {
405
405
  await pushYamlDeclarations(config, projectSlug, yamlConfig);
406
406
  }
@@ -270,10 +270,13 @@ async function runDev(opts) {
270
270
  // 3. Build ctx.vars from platform vars. A declared mapping is only readable
271
271
  // on ctx.vars when it is an OAuth connection (with proxy fields) OR has a
272
272
  // non-null resolved_value. Mappings with no value in this env are DROPPED
273
- // — and must NOT be counted in the summary (BUG #1).
273
+ // — and must NOT be counted in the summary (BUG #1). A dropped SECRET is
274
+ // reported separately (it's genuinely unavailable to local dev, not
275
+ // merely "unset" — the platform never resolves secret values to the CLI).
274
276
  const vars = {};
275
277
  let connectionCount = 0;
276
278
  let droppedCount = 0;
279
+ let droppedSecretCount = 0;
277
280
  for (const pv of platformVars) {
278
281
  if (pv.source_type === 'oauth_connection' && pv.proxy_url && pv.proxy_token && pv.connection_key) {
279
282
  vars[pv.env_name] = {
@@ -286,6 +289,9 @@ async function runDev(opts) {
286
289
  else if (pv.resolved_value != null) {
287
290
  vars[pv.env_name] = pv.resolved_value;
288
291
  }
292
+ else if (pv.is_secret) {
293
+ droppedSecretCount++;
294
+ }
289
295
  else {
290
296
  droppedCount++;
291
297
  }
@@ -308,6 +314,9 @@ async function runDev(opts) {
308
314
  if (droppedCount > 0) {
309
315
  summary += ` (${droppedCount} declared ${droppedCount === 1 ? 'var' : 'vars'} had no value in this env and ${droppedCount === 1 ? 'was' : 'were'} skipped)`;
310
316
  }
317
+ if (droppedSecretCount > 0) {
318
+ summary += ` (${droppedSecretCount} secret ${droppedSecretCount === 1 ? 'var is' : 'vars are'} not available to local dev — set a test value in your dev environment with \`solidactions env set\`, or pass values via \`-i\`.)`;
319
+ }
311
320
  out(summary);
312
321
  }
313
322
  else {
@@ -326,7 +335,21 @@ async function runDev(opts) {
326
335
  const entryPath = path_1.default.resolve(opts.entry);
327
336
  const _require = (0, module_1.createRequire)(entryPath);
328
337
  // Resolve via the installed (or linked) @solidactions/sdk package.
329
- const sdkTestingMain = _require.resolve('@solidactions/sdk/testing');
338
+ let sdkTestingMain;
339
+ try {
340
+ sdkTestingMain = _require.resolve('@solidactions/sdk/testing');
341
+ }
342
+ catch (e) {
343
+ if (e.code === 'MODULE_NOT_FOUND') {
344
+ err('Dependencies not installed — run `npm install` in the project directory first.');
345
+ return {
346
+ stdout: stdoutLines.join('\n'),
347
+ stderr: stderrLines.join('\n'),
348
+ result: { status: 'failed', error: e, phase: 'setup' },
349
+ };
350
+ }
351
+ throw e;
352
+ }
330
353
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
331
354
  const { createMockServer } = _require(sdkTestingMain);
332
355
  const mockServer = await createMockServer();
@@ -154,7 +154,7 @@ async function docsPushWithConfig(dir, options, config) {
154
154
  mcpResult = await (0, mcp_1.callDocsTool)(config, 'docs_vault', callArgs);
155
155
  }
156
156
  catch (e) {
157
- process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
157
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
158
158
  process.exit(1);
159
159
  }
160
160
  if (!mcpResult.ok) {
@@ -14,18 +14,20 @@ async function envDelete(keyOrProject, keyIfProject, options = {}) {
14
14
  const isProjectMode = keyIfProject !== undefined;
15
15
  const projectName = isProjectMode ? keyOrProject : undefined;
16
16
  const key = isProjectMode ? keyIfProject : keyOrProject;
17
+ const environment = options.env ?? 'dev';
18
+ const projectSlug = projectName && environment === 'production' ? projectName : `${projectName}-${environment}`;
17
19
  try {
18
20
  if (isProjectMode) {
19
- // Delete project variable mapping
20
- console.log(chalk_1.default.blue(`Deleting variable mapping "${key}" from project "${projectName}"...`));
21
- // First, get the mapping to find its ID
22
- const listResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectName}/variable-mappings`, {
21
+ // Delete project variable
22
+ console.log(chalk_1.default.blue(`Deleting variable "${key}" from project "${projectName}" (${environment})...`));
23
+ // First, get the variable to find its ID
24
+ const listResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings`, {
23
25
  headers: (0, api_1.getApiHeaders)(config),
24
26
  });
25
27
  const mappings = listResponse.data || [];
26
28
  const mapping = mappings.find((m) => m.env_name === key);
27
29
  if (!mapping) {
28
- console.error(chalk_1.default.red(`Variable mapping "${key}" not found in project "${projectName}".`));
30
+ console.error(chalk_1.default.red(`Variable "${key}" not found in project "${projectName}" (${environment}).`));
29
31
  process.exit(1);
30
32
  }
31
33
  // Confirm deletion unless --yes flag is provided
@@ -35,7 +37,7 @@ async function envDelete(keyOrProject, keyIfProject, options = {}) {
35
37
  name: 'confirm',
36
38
  message: mapping.is_yaml_declared
37
39
  ? `Clear YAML-declared variable "${key}"? (The mapping will be preserved but value cleared)`
38
- : `Delete variable mapping "${key}" from project "${projectName}"?`,
40
+ : `Delete variable "${key}" from project "${projectName}" (${environment})?`,
39
41
  initial: false,
40
42
  });
41
43
  if (!response.confirm) {
@@ -44,14 +46,14 @@ async function envDelete(keyOrProject, keyIfProject, options = {}) {
44
46
  }
45
47
  }
46
48
  // Delete the mapping
47
- await axios_1.default.delete(`${config.host}/api/v1/projects/${projectName}/variable-mappings/${mapping.id}`, {
49
+ await axios_1.default.delete(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings/${mapping.id}`, {
48
50
  headers: (0, api_1.getApiHeaders)(config),
49
51
  });
50
52
  if (mapping.is_yaml_declared) {
51
- console.log(chalk_1.default.green(`Variable mapping "${key}" cleared successfully.`));
53
+ console.log(chalk_1.default.green(`Variable "${key}" cleared successfully.`));
52
54
  }
53
55
  else {
54
- console.log(chalk_1.default.green(`Variable mapping "${key}" deleted successfully.`));
56
+ console.log(chalk_1.default.green(`Variable "${key}" deleted successfully.`));
55
57
  }
56
58
  }
57
59
  else {
@@ -93,7 +95,13 @@ async function envDelete(keyOrProject, keyIfProject, options = {}) {
93
95
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
94
96
  }
95
97
  else if (error.response.status === 404) {
96
- console.error(chalk_1.default.red(isProjectMode ? `Project "${projectName}" not found.` : `Variable "${key}" not found.`));
98
+ if (isProjectMode) {
99
+ const envsList = await (0, api_1.describeProjectEnvironments)(config, projectName);
100
+ console.error(chalk_1.default.red(`Project "${projectName}" has no ${environment} environment${envsList ? ` (exists in: ${envsList})` : ''}.`));
101
+ }
102
+ else {
103
+ console.error(chalk_1.default.red(`Variable "${key}" not found.`));
104
+ }
97
105
  }
98
106
  else {
99
107
  console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
@@ -7,6 +7,7 @@ exports.envList = envList;
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
+ const table_1 = require("../utils/table");
10
11
  /**
11
12
  * Format a value for display, with inherited badge if applicable.
12
13
  */
@@ -28,73 +29,77 @@ function formatEnvValue(value, source, isSecret) {
28
29
  }
29
30
  async function envList(projectName, options = {}) {
30
31
  const config = await (0, api_1.requireConfigWithWorkspace)();
32
+ const environment = options.env || 'dev';
31
33
  try {
32
34
  if (projectName) {
33
35
  // List project variable mappings
34
- const environment = options.env || 'dev';
35
36
  const projectSlug = environment === 'production'
36
37
  ? projectName
37
38
  : `${projectName}-${environment}`;
38
- console.log(chalk_1.default.blue(`Environment variables for project "${projectName}" (${environment}):`));
39
+ if (!options.json) {
40
+ console.log(chalk_1.default.blue(`Variables for project "${projectName}" (${environment}):`));
41
+ }
39
42
  const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings`, {
40
43
  headers: (0, api_1.getApiHeaders)(config),
41
44
  });
42
45
  const mappings = response.data || [];
46
+ if (options.json) {
47
+ console.log(JSON.stringify(mappings, null, 2));
48
+ return;
49
+ }
43
50
  if (mappings.length === 0) {
44
51
  console.log(chalk_1.default.gray('No variable mappings found.'));
45
52
  return;
46
53
  }
47
54
  console.log('');
48
- console.log(chalk_1.default.gray('KEY'.padEnd(28) + 'VALUE'.padEnd(24) + 'TYPE'.padEnd(14) + 'SOURCE'));
49
- console.log(chalk_1.default.gray('-'.repeat(90)));
50
- for (const mapping of mappings) {
55
+ const rows = mappings.map((mapping) => {
51
56
  const key = mapping.env_name || '?';
52
57
  // Value display (resolved_value includes global/oauth resolution)
53
58
  const rawValue = mapping.resolved_value ?? mapping.value;
54
- let value;
55
- if (mapping.is_secret) {
56
- value = chalk_1.default.yellow('••••••');
57
- }
58
- else if (rawValue) {
59
- value = rawValue.toString().substring(0, 22);
60
- }
61
- else {
62
- value = chalk_1.default.gray('-');
63
- }
59
+ const value = mapping.is_secret ? '••••••' : rawValue ? rawValue.toString() : '-';
64
60
  // Type & source (mirrors UI getMappingType)
65
61
  let type;
66
62
  let source;
67
63
  if (mapping.source_type === 'oauth_connection' && mapping.oauth_connection_id) {
68
- type = chalk_1.default.blue('oauth');
69
- source = chalk_1.default.blue(mapping.oauth_connection_name || 'OAuth');
64
+ type = 'oauth';
65
+ source = mapping.oauth_connection_name || 'OAuth';
70
66
  }
71
67
  else if (mapping.global_variable_key) {
72
- type = chalk_1.default.green('global');
73
- source = chalk_1.default.green(mapping.global_variable_key);
68
+ type = 'global';
69
+ source = mapping.global_variable_key;
74
70
  }
75
71
  else if (mapping.has_value) {
76
- type = chalk_1.default.gray('project var');
77
- source = chalk_1.default.gray('local');
72
+ type = 'project var';
73
+ source = 'local';
78
74
  }
79
75
  else {
80
- type = chalk_1.default.gray('-');
81
- source = chalk_1.default.gray('-');
76
+ type = '-';
77
+ source = '-';
82
78
  }
83
- console.log(key.padEnd(28) +
84
- value.padEnd(24) +
85
- type.padEnd(14) +
86
- source);
79
+ return [key, value, type, source];
80
+ });
81
+ const lines = (0, table_1.renderTable)(['KEY', 'VALUE', 'TYPE', 'SOURCE'], rows, { minWidths: [28, 24, 14, 0] });
82
+ console.log(chalk_1.default.gray(lines[0]));
83
+ console.log(chalk_1.default.gray(lines[1]));
84
+ for (const line of lines.slice(2)) {
85
+ console.log(line);
87
86
  }
88
87
  console.log('');
89
88
  console.log(chalk_1.default.gray(`${mappings.length} variable(s)`));
90
89
  }
91
90
  else {
92
91
  // List global variables with per-environment values
93
- console.log(chalk_1.default.blue('Global environment variables:'));
92
+ if (!options.json) {
93
+ console.log(chalk_1.default.blue('Global variables:'));
94
+ }
94
95
  const response = await axios_1.default.get(`${config.host}/api/v1/variables`, {
95
96
  headers: (0, api_1.getApiHeaders)(config),
96
97
  });
97
98
  const variables = response.data?.data || [];
99
+ if (options.json) {
100
+ console.log(JSON.stringify(variables, null, 2));
101
+ return;
102
+ }
98
103
  if (variables.length === 0) {
99
104
  console.log(chalk_1.default.gray('No global variables found.'));
100
105
  return;
@@ -185,7 +190,13 @@ async function envList(projectName, options = {}) {
185
190
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
186
191
  }
187
192
  else if (error.response.status === 404) {
188
- console.error(chalk_1.default.red(projectName ? `Project "${projectName}" not found for the specified environment.` : 'Resource not found.'));
193
+ if (projectName) {
194
+ const envsList = await (0, api_1.describeProjectEnvironments)(config, projectName);
195
+ console.error(chalk_1.default.red(`Project "${projectName}" has no ${environment} environment${envsList ? ` (exists in: ${envsList})` : ''}.`));
196
+ }
197
+ else {
198
+ console.error(chalk_1.default.red('Resource not found.'));
199
+ }
189
200
  }
190
201
  else {
191
202
  console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
@@ -35,7 +35,7 @@ async function envPull(projectName, options = {}) {
35
35
  // Determine output file
36
36
  const outputFile = options.output || (environment === 'production' ? '.env' : `.env.${environment}`);
37
37
  const outputPath = path_1.default.resolve(outputFile);
38
- console.log(chalk_1.default.blue(`Pulling environment variables from "${projectName}" (${environment})...`));
38
+ console.log(chalk_1.default.blue(`Pulling variables from "${projectName}" (${environment})...`));
39
39
  try {
40
40
  // First, check if there are any secrets
41
41
  const checkResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?resolve_oauth=true`, {
@@ -43,7 +43,7 @@ async function envPull(projectName, options = {}) {
43
43
  });
44
44
  const mappings = checkResponse.data || [];
45
45
  if (mappings.length === 0) {
46
- console.log(chalk_1.default.yellow('No environment variables found for this project.'));
46
+ console.log(chalk_1.default.yellow('No variables found for this project.'));
47
47
  process.exit(0);
48
48
  }
49
49
  // Check for secrets (skip confirmation for --update-oauth since OAuth tokens are always secrets)
@@ -68,7 +68,7 @@ async function envPull(projectName, options = {}) {
68
68
  console.log(chalk_1.default.yellow('No OAuth variables found for this project.'));
69
69
  process.exit(0);
70
70
  }
71
- // Build OAuth env var names set and their formatted lines
71
+ // Build OAuth variable names set and their formatted lines
72
72
  const oauthKeySet = new Set(oauthVars.map((v) => v.env_name));
73
73
  const oauthLines = [];
74
74
  for (const variable of oauthVars) {
@@ -146,7 +146,9 @@ async function envPull(projectName, options = {}) {
146
146
  }
147
147
  // Full pull: Build .env file content
148
148
  const lines = [];
149
- lines.push(`# Environment variables for ${projectName} (${environment})`);
149
+ lines.push(`# Variables pulled from SolidActions for ${projectName} / ${environment}.`);
150
+ lines.push('# NOTE: this .env is NOT read by `solidactions dev`. Local dev vars come from');
151
+ lines.push('# the platform (dev fetches them with `-e <env>`); pass ad-hoc values with `-i`.');
150
152
  lines.push(`# Generated by solidactions env pull on ${new Date().toISOString()}`);
151
153
  lines.push('');
152
154
  let count = 0;
@@ -37,7 +37,7 @@ async function envPush(projectName, sourcePath, options = {}) {
37
37
  const envFilePath = path_1.default.join(sourceDir, envFileName);
38
38
  if (!fs_1.default.existsSync(envFilePath)) {
39
39
  console.error(chalk_1.default.red(`${envFileName} not found in ${sourceDir}`));
40
- console.log(chalk_1.default.gray(`Create ${envFileName} with your environment variables, or use a different --env.`));
40
+ console.log(chalk_1.default.gray(`Create ${envFileName} with your variables, or use a different --env.`));
41
41
  process.exit(1);
42
42
  }
43
43
  // Parse the .env file
@@ -53,7 +53,7 @@ async function envPush(projectName, sourcePath, options = {}) {
53
53
  }
54
54
  else {
55
55
  if (declaredVars.size === 0) {
56
- console.log(chalk_1.default.yellow('No environment variables declared in solidactions.yaml.'));
56
+ console.log(chalk_1.default.yellow('No variables declared in solidactions.yaml.'));
57
57
  console.log(chalk_1.default.gray('Use --include-undeclared to push all vars from the .env file.'));
58
58
  process.exit(0);
59
59
  }
@@ -85,7 +85,7 @@ async function envPush(projectName, sourcePath, options = {}) {
85
85
  const projectSlug = environment === 'production'
86
86
  ? projectName
87
87
  : `${projectName}-${environment}`;
88
- console.log(chalk_1.default.blue(`Pushing env vars to "${projectName}" (${environment})...`));
88
+ console.log(chalk_1.default.blue(`Pushing variables to "${projectName}" (${environment})...`));
89
89
  // Fetch current server state
90
90
  let serverMappings = [];
91
91
  try {
@@ -200,7 +200,7 @@ async function envPush(projectName, sourcePath, options = {}) {
200
200
  console.log(chalk_1.default.gray(` ${created} created, ${updated} updated` + (toSkip.length > 0 ? `, ${toSkip.length} skipped` : '')));
201
201
  }
202
202
  catch (error) {
203
- console.error(chalk_1.default.red('Failed to push environment variables:'), error.response?.data?.message || error.message);
203
+ console.error(chalk_1.default.red('Failed to push variables:'), error.response?.data?.message || error.message);
204
204
  process.exit(1);
205
205
  }
206
206
  }
@@ -49,6 +49,10 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
49
49
  const projectName = keyOrProject;
50
50
  const key = valueOrKey;
51
51
  const value = valueIfProject;
52
+ if (!(0, env_1.isValidEnvName)(key)) {
53
+ console.error(chalk_1.default.red((0, env_1.envNameError)(key)));
54
+ process.exit(1);
55
+ }
52
56
  if ((0, env_1.isReservedEnvName)(key)) {
53
57
  console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(key)));
54
58
  process.exit(1);
@@ -105,10 +109,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
105
109
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
106
110
  }
107
111
  else if (error.response.status === 404) {
108
- console.error(chalk_1.default.red(`Project "${projectSlug}" not found.`));
112
+ const envsList = await (0, api_1.describeProjectEnvironments)(config, projectName);
113
+ console.error(chalk_1.default.red(`Project "${projectName}" has no ${environment} environment${envsList ? ` (exists in: ${envsList})` : ''}.`));
109
114
  }
110
115
  else if (error.response.status === 422) {
111
- console.error(chalk_1.default.red('Validation error:'), error.response.data);
116
+ console.error(chalk_1.default.red((0, api_1.formatValidationError)(error.response.data)));
112
117
  }
113
118
  else {
114
119
  console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
@@ -124,6 +129,10 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
124
129
  // Global mode: solidactions env set <key> <value>
125
130
  const key = keyOrProject;
126
131
  const value = valueOrKey;
132
+ if (!(0, env_1.isValidEnvName)(key)) {
133
+ console.error(chalk_1.default.red((0, env_1.envNameError)(key)));
134
+ process.exit(1);
135
+ }
127
136
  if ((0, env_1.isReservedEnvName)(key)) {
128
137
  console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(key)));
129
138
  process.exit(1);
@@ -218,7 +227,7 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
218
227
  console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
219
228
  }
220
229
  else if (error.response.status === 422) {
221
- console.error(chalk_1.default.red('Validation error:'), error.response.data.message || error.response.data.errors);
230
+ console.error(chalk_1.default.red((0, api_1.formatValidationError)(error.response.data)));
222
231
  }
223
232
  else {
224
233
  console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
@@ -49,6 +49,22 @@ async function init(directory, options = {}) {
49
49
  console.error(chalk_1.default.gray('Run in a fresh directory, or use `solidactions ai init` to add AI tooling to an existing project.'));
50
50
  process.exit(1);
51
51
  }
52
+ // Validate flags BEFORE any network fetch/write — previously this check
53
+ // lived only inside aiInit(), which runs after the template files are
54
+ // already fetched and written.
55
+ if (installSkills && options.claude && options.agents) {
56
+ console.error(chalk_1.default.red('Please specify only one of --claude or --agents'));
57
+ process.exit(1);
58
+ }
59
+ // Resolve the AI-helper target (CLAUDE.md vs AGENTS.md) UP FRONT, before
60
+ // writing any template file. Previously this decision was made inside
61
+ // aiInit(), AFTER the scaffold was already written — in a non-TTY shell
62
+ // with no --claude/--agents flag, the resulting unanswerable prompt left
63
+ // a half-scaffolded project (no CLAUDE.md/AGENTS.md, no .claude/skills/).
64
+ let aiHelperTarget;
65
+ if (installSkills) {
66
+ aiHelperTarget = await (0, ai_init_1.resolveAiHelperTarget)({ claude: options.claude, agents: options.agents });
67
+ }
52
68
  console.log(chalk_1.default.blue(`Scaffolding "${projectName}" in ${targetDir}...`));
53
69
  for (const [remoteSuffix, localSuffix] of exports.TEMPLATE_FILES) {
54
70
  const remotePath = `${TEMPLATE_PREFIX}/${remoteSuffix}`;
@@ -62,7 +78,7 @@ async function init(directory, options = {}) {
62
78
  if (installSkills) {
63
79
  console.log('');
64
80
  process.chdir(targetDir);
65
- await (0, ai_init_1.aiInit)({ claude: options.claude, agents: options.agents });
81
+ await (0, ai_init_1.aiInit)({ claude: options.claude, agents: options.agents }, aiHelperTarget);
66
82
  }
67
83
  console.log('');
68
84
  console.log(chalk_1.default.green(`✓ Project "${projectName}" scaffolded.`));
@@ -71,6 +87,7 @@ async function init(directory, options = {}) {
71
87
  if (directory) {
72
88
  console.log(chalk_1.default.gray(` cd ${directory}`));
73
89
  }
90
+ console.log(chalk_1.default.gray(' npm install'));
74
91
  console.log(chalk_1.default.gray(` solidactions project deploy ${projectName} -e production`));
75
92
  console.log(chalk_1.default.gray(` # If your workflow uses a webhook, retrieve its auto-generated secret`));
76
93
  console.log(chalk_1.default.gray(` # and set that value in your sender (e.g. Telegram secret_token):`));