@solidactions/cli 1.22.0 → 1.23.1

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).
@@ -115,7 +117,7 @@ Use `solidactions <command> --help` for full flag details on any command.
115
117
 
116
118
  | Command | Key Flags | Description |
117
119
  |---------|-----------|-------------|
118
- | `env set <key> <value>` | `-s`, `-y`, `--staging-value`, `--dev-value` | Set global variable (warns before overwriting) |
120
+ | `env set <key> <value> --global` | `-s`, `-y`, `--staging-value`, `--dev-value`, `--global` | Set global variable (warns before overwriting) |
119
121
  | `env set <project> <key> <value>` | `-e`, `-s`, `-y` | Set project variable (warns before overwriting) |
120
122
  | `env list [project]` | `-e` | List variables |
121
123
  | `env delete <key-or-project> [key]` | `-y` | Delete a variable |
@@ -25,7 +25,7 @@ async function envMap(projectName, projectKey, globalKey, options = {}) {
25
25
  const globalVar = variables.find((v) => v.key === globalKey);
26
26
  if (!globalVar) {
27
27
  console.error(chalk_1.default.red(`Global variable "${globalKey}" not found.`));
28
- console.log(chalk_1.default.gray('Create it with: solidactions env set ' + globalKey + ' <value>'));
28
+ console.log(chalk_1.default.gray('Create it with: solidactions env set ' + globalKey + ' <value> --global'));
29
29
  process.exit(1);
30
30
  }
31
31
  // Check if project key already has a mapping
@@ -17,15 +17,33 @@ function isNonTty() {
17
17
  }
18
18
  /** Printed before a 2-arg (global-scope) write — Jordan's runs 3-7 footgun. */
19
19
  exports.GLOBAL_ENV_SCOPE_NOTE = [
20
- 'Note: no project specified — creating a GLOBAL variable.',
20
+ 'Note: --global specified — creating a GLOBAL variable.',
21
21
  " Global variables are NOT visible to a project's plain `env:` YAML declarations",
22
22
  ' unless you map them (`solidactions env map …`).',
23
23
  ' For a project variable, use: solidactions env set <project> KEY value',
24
24
  ].join('\n');
25
25
  async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
26
- const config = await (0, api_1.requireConfigWithWorkspace)();
27
26
  // Detect mode based on arguments
28
27
  const isProjectMode = valueIfProject !== undefined;
28
+ // Jordan Wall #4 (Sweep C, → 1.23.0): a 2-positional-arg call used to fall
29
+ // through to GLOBAL scope silently, so a typo'd `env set KEY VALUE` (meant
30
+ // for a project) wrote a global var the workflow never reads. Global
31
+ // writes now require an explicit --global. Runs before any
32
+ // config/network work so a plain argument mistake never needs a login.
33
+ if (!isProjectMode && !options.global) {
34
+ process.stderr.write(chalk_1.default.red('env set needs either a project or --global — pick one:\n' +
35
+ ' solidactions env set KEY VALUE --global (global variable)\n' +
36
+ ' solidactions env set <project> KEY VALUE (project variable)\n'));
37
+ process.exit(1);
38
+ }
39
+ // --global never takes a project positional — reject the ambiguous combo too.
40
+ if (isProjectMode && options.global) {
41
+ process.stderr.write(chalk_1.default.red('env set: --global does not take a project argument — pick one:\n' +
42
+ ' solidactions env set KEY VALUE --global (global variable)\n' +
43
+ ' solidactions env set <project> KEY VALUE (project variable)\n'));
44
+ process.exit(1);
45
+ }
46
+ const config = await (0, api_1.requireConfigWithWorkspace)();
29
47
  if (isProjectMode) {
30
48
  // Project mode: solidactions env set <project> <key> <value>
31
49
  const projectName = keyOrProject;
@@ -7,10 +7,12 @@ 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"));
14
16
  const chalk_1 = __importDefault(require("chalk"));
15
17
  const api_1 = require("../utils/api");
16
18
  const workspaces_1 = require("./workspaces");
@@ -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 [
@@ -55,19 +69,28 @@ async function login(apiKey, options) {
55
69
  process.exit(1);
56
70
  }
57
71
  // Determine target location.
58
- const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global });
72
+ const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global }, undefined, LOGIN_REFUSAL_MESSAGE);
59
73
  const targetPath = (0, config_write_target_1.pathForTarget)(target);
60
74
  console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
61
75
  for (const line of loginHostLines(resolved)) {
62
76
  console.log(resolved.isDefault ? chalk_1.default.yellow(line) : chalk_1.default.gray(line));
63
77
  }
64
- if ((0, config_1.readConfigFile)(targetPath)) {
65
- console.log(chalk_1.default.yellow(`Existing config at ${targetPath} will be overwritten.`));
66
- }
67
78
  const config = {
68
79
  host,
69
80
  apiKey: apiKey.trim(),
70
81
  };
82
+ const existingRaw = fs_1.default.existsSync(targetPath) ? fs_1.default.readFileSync(targetPath, 'utf-8') : null;
83
+ const wouldChange = existingRaw !== null && existingRaw.trim() !== JSON.stringify(config, null, 2).trim();
84
+ if (wouldChange) {
85
+ const backupPath = backupPathFor(targetPath);
86
+ const proceed = await (0, config_write_target_1.confirmOverwrite)(targetPath, backupPath);
87
+ if (!proceed) {
88
+ console.log(chalk_1.default.yellow('Aborted. No changes were made.'));
89
+ process.exit(0);
90
+ }
91
+ fs_1.default.copyFileSync(targetPath, backupPath);
92
+ console.log(chalk_1.default.gray(`Backup saved to ${backupPath}`));
93
+ }
71
94
  (0, config_1.writeConfigFile)(targetPath, config);
72
95
  if (target === 'local') {
73
96
  await (0, config_write_target_1.ensureGitignoreCovers)(process.cwd(), !!options.gitignore);
package/dist/index.js CHANGED
@@ -244,6 +244,7 @@ env
244
244
  .argument('[value]', 'Variable value (when first arg is project)')
245
245
  .option('-s, --secret', 'Mark as encrypted secret')
246
246
  .option('-e, --env <environment>', 'Target environment (production/staging/dev)', 'dev')
247
+ .option('--global', 'Set a global variable (no project) — required for the 2-arg form')
247
248
  .option('--staging-value <value>', 'Staging environment value (global only)')
248
249
  .option('--dev-value <value>', 'Dev environment value (global only)')
249
250
  .option('--staging-inherit', 'Staging inherits from production (global only)')
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.decideWriteTarget = decideWriteTarget;
7
7
  exports.pathForTarget = pathForTarget;
8
+ exports.confirmOverwrite = confirmOverwrite;
8
9
  exports.ensureGitignoreCovers = ensureGitignoreCovers;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
@@ -19,7 +20,7 @@ const config_1 = require("./config");
19
20
  * - Else if TTY, prompt.
20
21
  * - Else error and exit.
21
22
  */
22
- async function decideWriteTarget(options, promptLabel = 'Save config locally (./.solidactions) or globally (~/.solidactions)? [global] ') {
23
+ async function decideWriteTarget(options, promptLabel = 'Save config locally (./.solidactions) or globally (~/.solidactions)? [global] ', refusalMessage = 'Refusing to write config non-interactively. Pass --local or --global.') {
23
24
  if (options.local && options.global) {
24
25
  console.error(chalk_1.default.red('Error: --local and --global are mutually exclusive.'));
25
26
  process.exit(1);
@@ -45,12 +46,34 @@ async function decideWriteTarget(options, promptLabel = 'Save config locally (./
45
46
  rl.close();
46
47
  }
47
48
  }
48
- console.error(chalk_1.default.red('Refusing to write config non-interactively. Pass --local or --global.'));
49
+ console.error(chalk_1.default.red(refusalMessage));
49
50
  process.exit(1);
50
51
  }
51
52
  function pathForTarget(target) {
52
53
  return target === 'local' ? (0, config_1.getLocalConfigPath)() : (0, config_1.getGlobalConfigPath)();
53
54
  }
55
+ /**
56
+ * Notify (and, on a TTY, confirm) that an existing config file is about to be
57
+ * overwritten. A backup will be written to `backupPath` regardless of mode —
58
+ * this always says so. Non-interactive callers (CI, agents) proceed
59
+ * automatically so they never wedge; an interactive TTY additionally asks a
60
+ * y/N confirm (default N).
61
+ */
62
+ async function confirmOverwrite(existingPath, backupPath) {
63
+ const notice = `Existing config at ${existingPath} will be overwritten (backup will be saved to ${backupPath}).`;
64
+ if (!process.stdin.isTTY) {
65
+ console.log(chalk_1.default.yellow(notice));
66
+ return true;
67
+ }
68
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
69
+ try {
70
+ const answer = await new Promise((resolve) => rl.question(chalk_1.default.yellow(`${notice} Continue? [y/N] `), resolve));
71
+ return answer.trim().toLowerCase().startsWith('y');
72
+ }
73
+ finally {
74
+ rl.close();
75
+ }
76
+ }
54
77
  /**
55
78
  * Ensure `.solidactions/` is in the target directory's `.gitignore`.
56
79
  * Idempotent. Skips silently if pattern is already covered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.22.0",
3
+ "version": "1.23.1",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {