@solidactions/cli 1.2.0 → 1.3.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
@@ -18,6 +18,48 @@ solidactions run start <project> <workflow> # Trigger a workflow
18
18
  solidactions run view <run-id> # Inspect a run
19
19
  ```
20
20
 
21
+ ## Configuration
22
+
23
+ The CLI stores `host`, `apiKey`, and `workspaceId` in a JSON config file. Two locations are supported:
24
+
25
+ - **Global** — `~/.solidactions/config.json`. Used by default, shared across all folders for a single user.
26
+ - **Local** — `./.solidactions/config.json`. Scoped to a project folder; takes precedence over the global file when running commands from inside that folder (or any subdirectory — the CLI walks up looking for it).
27
+
28
+ ### Resolution order
29
+
30
+ For each field (`host`, `apiKey`, `workspaceId`), the CLI resolves independently in this order:
31
+
32
+ 1. Environment variables: `SOLIDACTIONS_HOST`, `SOLIDACTIONS_API_KEY`, `SOLIDACTIONS_WORKSPACE_ID`
33
+ 2. Nearest local `./.solidactions/config.json` (walking up from cwd)
34
+ 3. Global `~/.solidactions/config.json`
35
+
36
+ You can mix: e.g., set only `SOLIDACTIONS_WORKSPACE_ID` in the environment while letting `host` and `apiKey` come from a file.
37
+
38
+ ### `solidactions init` flags
39
+
40
+ - `--local` — write config to `./.solidactions/config.json` in the current folder.
41
+ - `--global` — write config to `~/.solidactions/config.json` (today's default).
42
+ - `--gitignore` — with `--local`, auto-add `.solidactions/` to `.gitignore` without prompting.
43
+
44
+ In interactive shells, `init` without `--local`/`--global` prompts for a location. In non-interactive contexts, one of the flags is required.
45
+
46
+ ### `solidactions logout` flags
47
+
48
+ - `--local` — remove only the nearest local config (walks up from cwd).
49
+ - `--global` — remove only the global config.
50
+ - Bare `logout` — removes the nearest local if present, otherwise removes global.
51
+
52
+ ### Debugging resolution
53
+
54
+ Set `SOLIDACTIONS_DEBUG=1` on any command to print the resolved configuration and per-field sources to stderr before the command runs. `solidactions whoami` also shows this information.
55
+
56
+ ### Use case: multiple AI agents in parallel
57
+
58
+ If you run multiple AI coding agents in different project folders simultaneously, either:
59
+
60
+ - Run `solidactions init <key> --local` in each folder so each has its own config, or
61
+ - Set `SOLIDACTIONS_API_KEY` / `SOLIDACTIONS_WORKSPACE_ID` in the environment each agent uses (no files to share or stomp).
62
+
21
63
  ## Commands
22
64
 
23
65
  Use `solidactions <command> --help` for full flag details on any command.
@@ -11,41 +11,83 @@ exports.logout = logout;
11
11
  exports.whoami = whoami;
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
- const os_1 = __importDefault(require("os"));
14
+ const readline_1 = __importDefault(require("readline"));
15
15
  const chalk_1 = __importDefault(require("chalk"));
16
16
  const api_1 = require("../utils/api");
17
17
  const workspaces_1 = require("./workspaces");
18
- const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.solidactions');
19
- const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
18
+ const config_1 = require("../utils/config");
20
19
  function getConfig() {
21
- if (!fs_1.default.existsSync(CONFIG_FILE)) {
22
- return null;
23
- }
20
+ const resolved = (0, config_1.resolveConfig)();
21
+ return resolved ? resolved.config : null;
22
+ }
23
+ function saveConfig(config) {
24
+ const resolved = (0, config_1.resolveConfig)();
25
+ const targetPath = resolved ? resolved.activePath : (0, config_1.getGlobalConfigPath)();
26
+ (0, config_1.writeConfigFile)(targetPath, config);
27
+ }
28
+ function clearConfig() {
29
+ (0, config_1.removeConfigFile)((0, config_1.getGlobalConfigPath)());
30
+ }
31
+ async function promptLocation() {
32
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
24
33
  try {
25
- const config = JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, 'utf-8'));
26
- // Handle legacy config format (token -> apiKey)
27
- if (config.token && !config.apiKey) {
28
- config.apiKey = config.token;
34
+ while (true) {
35
+ const answer = await new Promise((resolve) => {
36
+ rl.question(chalk_1.default.blue('Save config locally (./.solidactions) or globally (~/.solidactions)? [global] '), resolve);
37
+ });
38
+ const normalized = answer.trim().toLowerCase();
39
+ if (normalized === '' || normalized === 'global' || normalized === 'g')
40
+ return 'global';
41
+ if (normalized === 'local' || normalized === 'l')
42
+ return 'local';
43
+ console.log(chalk_1.default.yellow("Please answer 'local' or 'global' (or press Enter for global)."));
29
44
  }
30
- return config;
31
45
  }
32
- catch {
33
- return null;
46
+ finally {
47
+ rl.close();
34
48
  }
35
49
  }
36
- function saveConfig(config) {
37
- if (!fs_1.default.existsSync(CONFIG_DIR)) {
38
- fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
50
+ async function ensureGitignoreCovers(targetDir, auto) {
51
+ const gitignorePath = path_1.default.join(targetDir, '.gitignore');
52
+ const patternToAdd = '.solidactions/';
53
+ let existing = '';
54
+ if (fs_1.default.existsSync(gitignorePath)) {
55
+ existing = fs_1.default.readFileSync(gitignorePath, 'utf-8');
56
+ const lines = existing.split('\n').map((l) => l.trim());
57
+ const isCovered = lines.some((line) => {
58
+ const normalized = line
59
+ .replace(/^\*\*\//, '')
60
+ .replace(/^\//, '')
61
+ .replace(/\/(\*\*|\*)?$/, '');
62
+ return normalized === '.solidactions';
63
+ });
64
+ if (isCovered) {
65
+ return; // already covered
66
+ }
39
67
  }
40
- fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
41
- }
42
- function clearConfig() {
43
- if (fs_1.default.existsSync(CONFIG_FILE)) {
44
- fs_1.default.unlinkSync(CONFIG_FILE);
68
+ let shouldAdd = auto;
69
+ if (!shouldAdd && process.stdin.isTTY) {
70
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
71
+ const answer = await new Promise((resolve) => {
72
+ rl.question(chalk_1.default.yellow(`Local config contains an API key. Add \`.solidactions/\` to ${gitignorePath}? [Y/n] `), resolve);
73
+ });
74
+ rl.close();
75
+ shouldAdd = !(answer.trim().toLowerCase().startsWith('n'));
76
+ }
77
+ if (!shouldAdd) {
78
+ console.log(chalk_1.default.yellow(`Skipping .gitignore update. Remember: ${path_1.default.join(targetDir, '.solidactions', 'config.json')} contains your API key.`));
79
+ return;
80
+ }
81
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
82
+ try {
83
+ fs_1.default.writeFileSync(gitignorePath, `${existing}${prefix}${patternToAdd}\n`);
84
+ console.log(chalk_1.default.green(`Added \`${patternToAdd}\` to ${gitignorePath}.`));
85
+ }
86
+ catch (err) {
87
+ console.log(chalk_1.default.yellow(`Could not update ${gitignorePath}: ${err.message}. Add \`.solidactions/\` to it manually — ${path_1.default.join(targetDir, '.solidactions', 'config.json')} contains your API key.`));
45
88
  }
46
89
  }
47
90
  async function init(apiKey, options) {
48
- // Determine host
49
91
  let host;
50
92
  if (options.host) {
51
93
  host = options.host;
@@ -56,24 +98,48 @@ async function init(apiKey, options) {
56
98
  else {
57
99
  host = 'https://app.solidactions.com';
58
100
  }
59
- // Validate API key format (should be a Sanctum token)
60
101
  if (!apiKey || apiKey.trim().length === 0) {
61
102
  console.error(chalk_1.default.red('Error: API key is required.'));
62
103
  console.log(chalk_1.default.gray('Generate an API key at: ') + chalk_1.default.blue(`${host}/settings/api-keys`));
63
104
  process.exit(1);
64
105
  }
106
+ // Determine target location.
107
+ let target;
108
+ if (options.local && options.global) {
109
+ console.error(chalk_1.default.red('Error: --local and --global are mutually exclusive.'));
110
+ process.exit(1);
111
+ }
112
+ else if (options.local) {
113
+ target = 'local';
114
+ }
115
+ else if (options.global) {
116
+ target = 'global';
117
+ }
118
+ else if (process.stdin.isTTY) {
119
+ target = await promptLocation();
120
+ }
121
+ else {
122
+ console.error(chalk_1.default.red('Refusing to init non-interactively. Pass --local or --global.'));
123
+ process.exit(1);
124
+ }
125
+ const targetPath = target === 'local' ? (0, config_1.getLocalConfigPath)() : (0, config_1.getGlobalConfigPath)();
65
126
  console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
66
127
  console.log(chalk_1.default.gray(`Host: ${host}`));
67
- // Save the configuration
128
+ if ((0, config_1.readConfigFile)(targetPath)) {
129
+ console.log(chalk_1.default.yellow(`Existing config at ${targetPath} will be overwritten.`));
130
+ }
68
131
  const config = {
69
132
  host,
70
133
  apiKey: apiKey.trim(),
71
134
  };
72
- saveConfig(config);
135
+ (0, config_1.writeConfigFile)(targetPath, config);
136
+ if (target === 'local') {
137
+ await ensureGitignoreCovers(process.cwd(), !!options.gitignore);
138
+ }
73
139
  console.log(chalk_1.default.green('CLI initialized successfully!'));
74
- console.log(chalk_1.default.gray(`Configuration saved to ${CONFIG_FILE}`));
140
+ console.log(chalk_1.default.gray(`Configuration saved to ${targetPath}`));
75
141
  console.log('');
76
- // Set workspaceexplicit flag or interactive prompt
142
+ // Workspace selectionensureWorkspaceSelected writes to the right file via the resolver.
77
143
  try {
78
144
  if (options.workspace) {
79
145
  await (0, workspaces_1.workspaceSet)(options.workspace);
@@ -91,23 +157,55 @@ async function init(apiKey, options) {
91
157
  console.log(chalk_1.default.gray(' solidactions run start <proj> <wf> Run a workflow'));
92
158
  console.log(chalk_1.default.gray(' solidactions run list List recent runs'));
93
159
  }
94
- async function logout() {
95
- if (fs_1.default.existsSync(CONFIG_FILE)) {
96
- clearConfig();
97
- console.log(chalk_1.default.green('Logged out successfully.'));
160
+ function logout(options = {}) {
161
+ if (options.local && options.global) {
162
+ console.error(chalk_1.default.red('Error: --local and --global are mutually exclusive.'));
163
+ process.exit(1);
164
+ }
165
+ const globalPath = (0, config_1.getGlobalConfigPath)();
166
+ const localPath = (0, config_1.findLocalConfigPath)(process.cwd());
167
+ let targetPath;
168
+ if (options.local) {
169
+ targetPath = localPath;
170
+ if (!targetPath) {
171
+ console.error(chalk_1.default.red(`No local config found in ${process.cwd()} or any parent directory.`));
172
+ process.exit(1);
173
+ }
174
+ }
175
+ else if (options.global) {
176
+ targetPath = globalPath;
98
177
  }
99
178
  else {
100
- console.log(chalk_1.default.gray('Not logged in.'));
179
+ targetPath = localPath ?? globalPath;
180
+ }
181
+ const removed = (0, config_1.removeConfigFile)(targetPath);
182
+ if (removed) {
183
+ console.log(chalk_1.default.green(`Logged out. Removed ${targetPath}`));
184
+ }
185
+ else {
186
+ console.log(chalk_1.default.gray(`Not logged in (no config at ${targetPath}).`));
101
187
  }
102
188
  }
103
- async function whoami() {
104
- const config = getConfig();
105
- if (!config) {
189
+ function whoami() {
190
+ const resolved = (0, config_1.resolveConfig)();
191
+ if (!resolved || !resolved.config.apiKey) {
106
192
  console.log(chalk_1.default.yellow('Not initialized.'));
107
193
  console.log(chalk_1.default.gray('Run "solidactions init <api-key>" to configure.'));
108
194
  process.exit(1);
109
195
  }
196
+ const { config, sources } = resolved;
197
+ const maskedKey = config.apiKey.length > 12
198
+ ? `${config.apiKey.substring(0, 8)}...${config.apiKey.slice(-4)}`
199
+ : config.apiKey;
200
+ const fmt = (src) => {
201
+ if (src === 'env')
202
+ return chalk_1.default.gray('(from $SOLIDACTIONS_* env var)');
203
+ if (src === null)
204
+ return chalk_1.default.gray('(unset)');
205
+ return chalk_1.default.gray(`(from ${src})`);
206
+ };
110
207
  console.log(chalk_1.default.blue('Current configuration:'));
111
- console.log(` Host: ${config.host}`);
112
- console.log(` API Key: ${config.apiKey.substring(0, 8)}...${config.apiKey.slice(-4)}`);
208
+ console.log(` Host: ${config.host.padEnd(40)} ${fmt(sources.host)}`);
209
+ console.log(` API Key: ${maskedKey.padEnd(40)} ${fmt(sources.apiKey)}`);
210
+ console.log(` Workspace: ${(config.workspaceId ?? '').padEnd(40)} ${fmt(sources.workspaceId)}`);
113
211
  }
@@ -8,7 +8,7 @@ exports.workspaceSet = workspaceSet;
8
8
  const axios_1 = __importDefault(require("axios"));
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const api_1 = require("../utils/api");
11
- const init_1 = require("./init");
11
+ const config_1 = require("../utils/config");
12
12
  async function workspacesList() {
13
13
  const config = (0, api_1.requireConfig)();
14
14
  try {
@@ -53,8 +53,13 @@ async function workspacesList() {
53
53
  }
54
54
  }
55
55
  async function workspaceSet(workspaceId) {
56
- const config = (0, api_1.requireConfig)();
57
- // Validate the workspace exists and user has access
56
+ if (process.env.SOLIDACTIONS_WORKSPACE_ID) {
57
+ console.error(chalk_1.default.red('SOLIDACTIONS_WORKSPACE_ID is set in the environment; the change would not take effect. ' +
58
+ 'Unset the env var or edit the config file directly.'));
59
+ process.exit(1);
60
+ }
61
+ const resolved = (0, api_1.requireResolvedConfig)();
62
+ const config = resolved.config;
58
63
  try {
59
64
  const response = await axios_1.default.get(`${config.host}/api/v1/workspaces`, {
60
65
  headers: {
@@ -62,7 +67,6 @@ async function workspaceSet(workspaceId) {
62
67
  'Accept': 'application/json',
63
68
  },
64
69
  });
65
- // Flatten grouped response
66
70
  const grouped = response.data.workspaces || response.data.data || response.data;
67
71
  let allWorkspaces = [];
68
72
  if (typeof grouped === 'object' && !Array.isArray(grouped)) {
@@ -78,9 +82,10 @@ async function workspaceSet(workspaceId) {
78
82
  console.error(chalk_1.default.red(`Workspace "${workspaceId}" not found. Run \`solidactions workspace list\` to list available workspaces.`));
79
83
  process.exit(1);
80
84
  }
81
- config.workspaceId = workspace.id;
82
- (0, init_1.saveConfig)(config);
85
+ const updated = { ...config, workspaceId: workspace.id };
86
+ (0, config_1.writeConfigFile)(resolved.activePath, updated);
83
87
  console.log(chalk_1.default.green(`Workspace set to: ${workspace.name} (${workspace.id})`));
88
+ console.log(chalk_1.default.gray(`Saved to ${resolved.activePath}`));
84
89
  }
85
90
  catch (error) {
86
91
  console.error(chalk_1.default.red('Failed to set workspace:'), error.response?.data?.message || error.message);
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const chalk_1 = __importDefault(require("chalk"));
4
8
  const commander_1 = require("commander");
5
9
  const deploy_1 = require("./commands/deploy");
6
10
  const init_1 = require("./commands/init");
@@ -27,6 +31,28 @@ const workspaces_1 = require("./commands/workspaces");
27
31
  // eslint-disable-next-line @typescript-eslint/no-var-requires
28
32
  const pkg = require('../package.json');
29
33
  const program = new commander_1.Command();
34
+ if (process.env.SOLIDACTIONS_DEBUG === '1') {
35
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
36
+ const { resolveConfig } = require('./utils/config');
37
+ const resolved = resolveConfig();
38
+ if (resolved) {
39
+ const fmt = (src) => {
40
+ if (src === 'env')
41
+ return '(from $SOLIDACTIONS_* env var)';
42
+ if (src === null)
43
+ return '(unset)';
44
+ return `(from ${src})`;
45
+ };
46
+ process.stderr.write('[SOLIDACTIONS_DEBUG] resolved configuration:\n');
47
+ process.stderr.write(` host: ${resolved.config.host} ${fmt(resolved.sources.host)}\n`);
48
+ process.stderr.write(` apiKey: <redacted> ${fmt(resolved.sources.apiKey)}\n`);
49
+ process.stderr.write(` workspaceId: ${resolved.config.workspaceId ?? ''} ${fmt(resolved.sources.workspaceId)}\n`);
50
+ process.stderr.write(` activePath: ${resolved.activePath}\n`);
51
+ }
52
+ else {
53
+ process.stderr.write('[SOLIDACTIONS_DEBUG] no config resolvable\n');
54
+ }
55
+ }
30
56
  program
31
57
  .name('solidactions')
32
58
  .description('SolidActions CLI - Deploy and manage workflow automation')
@@ -41,14 +67,19 @@ program
41
67
  .option('--dev', 'Use local development server (http://localhost:8000)')
42
68
  .option('--host <url>', 'Custom API host URL')
43
69
  .option('--workspace <name-or-id>', 'Set workspace by name, slug, or ID (skips interactive prompt)')
44
- .action((apiKey, options) => {
45
- (0, init_1.init)(apiKey, options);
70
+ .option('--local', 'Save config to ./.solidactions/config.json in the current folder')
71
+ .option('--global', 'Save config to ~/.solidactions/config.json (default if prompted)')
72
+ .option('--gitignore', 'With --local, add .solidactions/ to .gitignore without prompting')
73
+ .action(async (apiKey, options) => {
74
+ await (0, init_1.init)(apiKey, options);
46
75
  });
47
76
  program
48
77
  .command('logout')
49
78
  .description('Remove saved credentials')
50
- .action(() => {
51
- (0, init_1.logout)();
79
+ .option('--local', 'Remove only the nearest local ./.solidactions/config.json')
80
+ .option('--global', 'Remove only ~/.solidactions/config.json')
81
+ .action((options) => {
82
+ (0, init_1.logout)(options);
52
83
  });
53
84
  program
54
85
  .command('whoami')
@@ -291,4 +322,7 @@ ai
291
322
  .option('--all', 'Install all available examples')
292
323
  .option('--overwrite', 'Overwrite existing examples without warning')
293
324
  .action((names, options) => { (0, ai_examples_1.aiExamples)(names, options); });
294
- program.parse();
325
+ program.parseAsync().catch((err) => {
326
+ console.error(chalk_1.default.red(err.message ?? String(err)));
327
+ process.exit(1);
328
+ });
package/dist/utils/api.js CHANGED
@@ -4,40 +4,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getApiHeaders = getApiHeaders;
7
- exports.ensureWorkspaceSelected = ensureWorkspaceSelected;
7
+ exports.requireResolvedConfig = requireResolvedConfig;
8
8
  exports.requireConfig = requireConfig;
9
+ exports.ensureWorkspaceSelected = ensureWorkspaceSelected;
9
10
  exports.requireConfigWithWorkspace = requireConfigWithWorkspace;
10
11
  const axios_1 = __importDefault(require("axios"));
11
12
  const chalk_1 = __importDefault(require("chalk"));
12
13
  const readline_1 = __importDefault(require("readline"));
13
- const init_1 = require("../commands/init");
14
- /**
15
- * Get standard API headers including X-Workspace-Id if configured.
16
- */
14
+ const config_1 = require("./config");
17
15
  function getApiHeaders(config, contentType) {
18
16
  const headers = {
19
17
  'Authorization': `Bearer ${config.apiKey}`,
20
18
  'Accept': 'application/json',
21
19
  };
22
- if (contentType) {
20
+ if (contentType)
23
21
  headers['Content-Type'] = contentType;
24
- }
25
- if (config.workspaceId) {
22
+ if (config.workspaceId)
26
23
  headers['X-Workspace-Id'] = config.workspaceId;
27
- }
28
24
  return headers;
29
25
  }
30
26
  /**
31
- * Ensure a workspace is selected. If not configured, fetches workspaces and auto-selects
32
- * (if only one) or prompts the user to choose.
33
- *
34
- * Returns the config with workspaceId set, or exits if no workspaces available.
27
+ * Get the full resolution (config + sources + activePath). Exits if nothing resolvable.
35
28
  */
29
+ function requireResolvedConfig() {
30
+ const resolved = (0, config_1.resolveConfig)();
31
+ if (!resolved || !resolved.config.apiKey) {
32
+ console.error(chalk_1.default.red('Not initialized. Run `solidactions init <api-key>` first.'));
33
+ process.exit(1);
34
+ }
35
+ return resolved;
36
+ }
37
+ function requireConfig() {
38
+ return requireResolvedConfig().config;
39
+ }
36
40
  async function ensureWorkspaceSelected(config) {
37
41
  if (config.workspaceId) {
38
42
  return config;
39
43
  }
40
- // Fetch workspaces from API (this endpoint doesn't require X-Workspace-Id)
44
+ // Re-resolve so we know whether a save would be redundant (env-provided) or meaningful (file-backed).
45
+ const resolved = (0, config_1.resolveConfig)();
46
+ const workspaceSource = resolved?.sources.workspaceId ?? null;
41
47
  let workspaces;
42
48
  try {
43
49
  const response = await axios_1.default.get(`${config.host}/api/v1/workspaces`, {
@@ -46,8 +52,6 @@ async function ensureWorkspaceSelected(config) {
46
52
  'Accept': 'application/json',
47
53
  },
48
54
  });
49
- // API returns { workspaces: { "Org Name": [{ id, name, role, tenant_name, ... }] } }
50
- // Flatten the grouped structure into a flat array
51
55
  const grouped = response.data.workspaces || response.data.teams || response.data.data || response.data;
52
56
  if (typeof grouped === 'object' && !Array.isArray(grouped)) {
53
57
  workspaces = [];
@@ -85,7 +89,6 @@ async function ensureWorkspaceSelected(config) {
85
89
  console.log(chalk_1.default.gray(`Auto-selected workspace: ${selected.name}`));
86
90
  }
87
91
  else {
88
- // Prompt user to select
89
92
  console.log(chalk_1.default.blue('\nSelect a workspace:\n'));
90
93
  workspaces.forEach((ws, i) => {
91
94
  console.log(` ${chalk_1.default.white(`${i + 1}.`)} ${ws.name} ${chalk_1.default.gray(`(${ws.org_name}, ${ws.role})`)}`);
@@ -103,26 +106,14 @@ async function ensureWorkspaceSelected(config) {
103
106
  }
104
107
  selected = workspaces[index];
105
108
  }
106
- // Save workspace selection to config
107
109
  config.workspaceId = selected.id;
108
- (0, init_1.saveConfig)(config);
109
- console.log(chalk_1.default.green(`Workspace set: ${selected.name}`));
110
- return config;
111
- }
112
- /**
113
- * Get config and ensure it's initialized. Exits if not.
114
- */
115
- function requireConfig() {
116
- const config = (0, init_1.getConfig)();
117
- if (!config?.apiKey) {
118
- console.error(chalk_1.default.red('Not initialized. Run `solidactions init <api-key>` first.'));
119
- process.exit(1);
110
+ if (workspaceSource !== 'env') {
111
+ const targetPath = resolved?.activePath ?? (0, config_1.getGlobalConfigPath)();
112
+ (0, config_1.writeConfigFile)(targetPath, config);
120
113
  }
114
+ console.log(chalk_1.default.green(`Workspace set: ${selected.name}`));
121
115
  return config;
122
116
  }
123
- /**
124
- * Get config with workspace selected. Use this for any command that needs workspace context.
125
- */
126
117
  async function requireConfigWithWorkspace() {
127
118
  const config = requireConfig();
128
119
  return ensureWorkspaceSelected(config);
@@ -0,0 +1,138 @@
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.getGlobalConfigPath = getGlobalConfigPath;
7
+ exports.getLocalConfigPath = getLocalConfigPath;
8
+ exports.findLocalConfigPath = findLocalConfigPath;
9
+ exports.readConfigFile = readConfigFile;
10
+ exports.writeConfigFile = writeConfigFile;
11
+ exports.removeConfigFile = removeConfigFile;
12
+ exports.resolveConfig = resolveConfig;
13
+ // src/utils/config.ts
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const os_1 = __importDefault(require("os"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const GLOBAL_DIR = path_1.default.join(os_1.default.homedir(), '.solidactions');
18
+ const GLOBAL_FILE = path_1.default.join(GLOBAL_DIR, 'config.json');
19
+ const LOCAL_DIR_NAME = '.solidactions';
20
+ const LOCAL_FILE_NAME = 'config.json';
21
+ function getGlobalConfigPath() {
22
+ return GLOBAL_FILE;
23
+ }
24
+ function getLocalConfigPath(cwd = process.cwd()) {
25
+ return path_1.default.join(cwd, LOCAL_DIR_NAME, LOCAL_FILE_NAME);
26
+ }
27
+ /**
28
+ * Walk up from startDir looking for `.solidactions/config.json`.
29
+ * Stops at filesystem root. **Skips `$HOME` itself** so the global config
30
+ * at `~/.solidactions/config.json` is never matched as a local hit.
31
+ * Returns the absolute path of the nearest local config, or null.
32
+ */
33
+ function findLocalConfigPath(startDir = process.cwd()) {
34
+ const home = os_1.default.homedir();
35
+ let dir = path_1.default.resolve(startDir);
36
+ while (true) {
37
+ if (dir !== home) {
38
+ const candidate = path_1.default.join(dir, LOCAL_DIR_NAME, LOCAL_FILE_NAME);
39
+ if (fs_1.default.existsSync(candidate)) {
40
+ return candidate;
41
+ }
42
+ }
43
+ const parent = path_1.default.dirname(dir);
44
+ if (parent === dir) {
45
+ return null;
46
+ }
47
+ dir = parent;
48
+ }
49
+ }
50
+ /**
51
+ * Read a config file. Normalizes the legacy `token` field into `apiKey`.
52
+ * Returns null if the file does not exist or cannot be parsed.
53
+ */
54
+ function readConfigFile(filePath) {
55
+ if (!fs_1.default.existsSync(filePath)) {
56
+ return null;
57
+ }
58
+ try {
59
+ const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
60
+ if (raw.token && !raw.apiKey) {
61
+ raw.apiKey = raw.token;
62
+ }
63
+ return raw;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /**
70
+ * Atomic write: writes to `<filePath>.tmp` and renames into place.
71
+ * Creates parent directory with mode 0o700 if missing.
72
+ * File is written with mode 0o600.
73
+ */
74
+ function writeConfigFile(filePath, config) {
75
+ const dir = path_1.default.dirname(filePath);
76
+ if (!fs_1.default.existsSync(dir)) {
77
+ fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
78
+ }
79
+ const tmp = `${filePath}.tmp`;
80
+ fs_1.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
81
+ fs_1.default.renameSync(tmp, filePath);
82
+ }
83
+ function removeConfigFile(filePath) {
84
+ if (!fs_1.default.existsSync(filePath)) {
85
+ return false;
86
+ }
87
+ fs_1.default.unlinkSync(filePath);
88
+ return true;
89
+ }
90
+ function readEnvOverrides() {
91
+ const env = {};
92
+ if (process.env.SOLIDACTIONS_HOST)
93
+ env.host = process.env.SOLIDACTIONS_HOST;
94
+ if (process.env.SOLIDACTIONS_API_KEY)
95
+ env.apiKey = process.env.SOLIDACTIONS_API_KEY;
96
+ if (process.env.SOLIDACTIONS_WORKSPACE_ID)
97
+ env.workspaceId = process.env.SOLIDACTIONS_WORKSPACE_ID;
98
+ return env;
99
+ }
100
+ /**
101
+ * Resolve config by merging three layers field-by-field (env > local > global).
102
+ * Returns null only when no source contributes an apiKey AND host (i.e. nothing usable).
103
+ * `activePath` is the file a write-mutating command should target: nearest local if present, else global.
104
+ */
105
+ function resolveConfig(cwd = process.cwd()) {
106
+ const env = readEnvOverrides();
107
+ const localPath = findLocalConfigPath(cwd);
108
+ const local = localPath ? readConfigFile(localPath) : null;
109
+ const global = readConfigFile(getGlobalConfigPath());
110
+ const pick = (key) => {
111
+ if (env[key] !== undefined)
112
+ return { value: env[key], source: 'env' };
113
+ if (local && local[key] !== undefined)
114
+ return { value: local[key], source: localPath };
115
+ if (global && global[key] !== undefined)
116
+ return { value: global[key], source: getGlobalConfigPath() };
117
+ return { value: undefined, source: null };
118
+ };
119
+ const host = pick('host');
120
+ const apiKey = pick('apiKey');
121
+ const workspaceId = pick('workspaceId');
122
+ if (!host.value && !apiKey.value) {
123
+ return null;
124
+ }
125
+ return {
126
+ config: {
127
+ host: (host.value ?? ''),
128
+ apiKey: (apiKey.value ?? ''),
129
+ workspaceId: workspaceId.value,
130
+ },
131
+ sources: {
132
+ host: host.source,
133
+ apiKey: apiKey.source,
134
+ workspaceId: workspaceId.source,
135
+ },
136
+ activePath: localPath ?? getGlobalConfigPath(),
137
+ };
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {