@solidactions/cli 1.8.0 → 1.9.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/dist/commands/deploy.js +20 -0
- package/dist/commands/login.js +14 -85
- package/dist/commands/workspaces.js +12 -33
- package/dist/index.js +24 -4
- package/dist/utils/api.js +33 -1
- package/dist/utils/config-write-target.js +101 -0
- package/dist/utils/config.js +78 -17
- package/dist/utils/workspace-lookup.js +62 -0
- package/package.json +60 -57
package/dist/commands/deploy.js
CHANGED
|
@@ -113,6 +113,17 @@ async function pushYamlDeclarations(config, projectSlug, yamlConfig) {
|
|
|
113
113
|
}
|
|
114
114
|
async function deploy(projectName, sourcePath, options = {}) {
|
|
115
115
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
116
|
+
let workspaceMismatchPrinted = false;
|
|
117
|
+
const printWorkspaceMismatchOnce = (error) => {
|
|
118
|
+
if (workspaceMismatchPrinted)
|
|
119
|
+
return;
|
|
120
|
+
const msg = error.response?.data?.message;
|
|
121
|
+
if (typeof msg === 'string' && /Project .+ not found in your active workspace/.test(msg)) {
|
|
122
|
+
console.error(chalk_1.default.yellow(msg));
|
|
123
|
+
console.error('');
|
|
124
|
+
workspaceMismatchPrinted = true;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
116
127
|
const sourceDir = sourcePath ? path_1.default.resolve(sourcePath) : process.cwd();
|
|
117
128
|
// Capture whether -e was explicitly passed by the caller. Commander leaves
|
|
118
129
|
// options.env as undefined when no default is set and the flag is omitted.
|
|
@@ -138,6 +149,11 @@ async function deploy(projectName, sourcePath, options = {}) {
|
|
|
138
149
|
catch (error) {
|
|
139
150
|
if (error.response?.status === 404) {
|
|
140
151
|
productionExists = false;
|
|
152
|
+
// App PR #128 returns "Project '<slug>' not found in your active workspace '<ws>'." on 404.
|
|
153
|
+
// The axios interceptor (utils/api.ts) already appended the "Did you mean to switch workspaces?"
|
|
154
|
+
// hint. Surface that augmented message so the user sees the hint before falling through to
|
|
155
|
+
// the first-deploy flow.
|
|
156
|
+
printWorkspaceMismatchOnce(error);
|
|
141
157
|
}
|
|
142
158
|
else {
|
|
143
159
|
// 5xx, network error, auth failure, etc. — fail conservatively rather
|
|
@@ -211,6 +227,10 @@ async function deploy(projectName, sourcePath, options = {}) {
|
|
|
211
227
|
}
|
|
212
228
|
catch (error) {
|
|
213
229
|
if (error.response?.status === 404) {
|
|
230
|
+
// App PR #128 returns "Project '<slug>' not found in your active workspace '<ws>'." on 404.
|
|
231
|
+
// Surface the augmented message (axios interceptor already appended the hint) before
|
|
232
|
+
// falling through to the --create / first-deploy flow.
|
|
233
|
+
printWorkspaceMismatchOnce(error);
|
|
214
234
|
// For non-production environments, require --create or give a clear hint
|
|
215
235
|
if (environment !== 'production' && !options.create) {
|
|
216
236
|
console.error(chalk_1.default.red(`\nProject "${projectName}" doesn't have a ${environment} environment.\n`));
|
package/dist/commands/login.js
CHANGED
|
@@ -9,13 +9,11 @@ exports.clearConfig = clearConfig;
|
|
|
9
9
|
exports.login = login;
|
|
10
10
|
exports.logout = logout;
|
|
11
11
|
exports.whoami = whoami;
|
|
12
|
-
const fs_1 = __importDefault(require("fs"));
|
|
13
|
-
const path_1 = __importDefault(require("path"));
|
|
14
|
-
const readline_1 = __importDefault(require("readline"));
|
|
15
12
|
const chalk_1 = __importDefault(require("chalk"));
|
|
16
13
|
const api_1 = require("../utils/api");
|
|
17
14
|
const workspaces_1 = require("./workspaces");
|
|
18
15
|
const config_1 = require("../utils/config");
|
|
16
|
+
const config_write_target_1 = require("../utils/config-write-target");
|
|
19
17
|
function getConfig() {
|
|
20
18
|
const resolved = (0, config_1.resolveConfig)();
|
|
21
19
|
return resolved ? resolved.config : null;
|
|
@@ -28,65 +26,6 @@ function saveConfig(config) {
|
|
|
28
26
|
function clearConfig() {
|
|
29
27
|
(0, config_1.removeConfigFile)((0, config_1.getGlobalConfigPath)());
|
|
30
28
|
}
|
|
31
|
-
async function promptLocation() {
|
|
32
|
-
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
-
try {
|
|
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)."));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
rl.close();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
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
|
-
}
|
|
67
|
-
}
|
|
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.`));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
29
|
async function login(apiKey, options) {
|
|
91
30
|
let host;
|
|
92
31
|
if (options.host) {
|
|
@@ -104,25 +43,8 @@ async function login(apiKey, options) {
|
|
|
104
43
|
process.exit(1);
|
|
105
44
|
}
|
|
106
45
|
// Determine target location.
|
|
107
|
-
|
|
108
|
-
|
|
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)();
|
|
46
|
+
const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global });
|
|
47
|
+
const targetPath = (0, config_write_target_1.pathForTarget)(target);
|
|
126
48
|
console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
|
|
127
49
|
console.log(chalk_1.default.gray(`Host: ${host}`));
|
|
128
50
|
if ((0, config_1.readConfigFile)(targetPath)) {
|
|
@@ -134,7 +56,7 @@ async function login(apiKey, options) {
|
|
|
134
56
|
};
|
|
135
57
|
(0, config_1.writeConfigFile)(targetPath, config);
|
|
136
58
|
if (target === 'local') {
|
|
137
|
-
await ensureGitignoreCovers(process.cwd(), !!options.gitignore);
|
|
59
|
+
await (0, config_write_target_1.ensureGitignoreCovers)(process.cwd(), !!options.gitignore);
|
|
138
60
|
}
|
|
139
61
|
console.log(chalk_1.default.green('Logged in successfully!'));
|
|
140
62
|
console.log(chalk_1.default.gray(`Configuration saved to ${targetPath}`));
|
|
@@ -204,12 +126,19 @@ function whoami() {
|
|
|
204
126
|
const fmt = (src) => {
|
|
205
127
|
if (src === 'env')
|
|
206
128
|
return chalk_1.default.gray('(from $SOLIDACTIONS_* env var)');
|
|
129
|
+
if (src === 'cli')
|
|
130
|
+
return chalk_1.default.gray('(from -w flag)');
|
|
207
131
|
if (src === null)
|
|
208
132
|
return chalk_1.default.gray('(unset)');
|
|
209
133
|
return chalk_1.default.gray(`(from ${src})`);
|
|
210
134
|
};
|
|
135
|
+
const workspaceLabel = config.workspace
|
|
136
|
+
? `${config.workspace}${config.workspaceId ? ` (${config.workspaceId})` : ''}`
|
|
137
|
+
: config.workspaceId
|
|
138
|
+
? `${config.workspaceId} (slug unknown — run 'workspace set <slug>' to populate)`
|
|
139
|
+
: '';
|
|
211
140
|
console.log(chalk_1.default.blue('Current configuration:'));
|
|
212
|
-
console.log(` Host: ${config.host.padEnd(
|
|
213
|
-
console.log(` API Key: ${maskedKey.padEnd(
|
|
214
|
-
console.log(` Workspace: ${
|
|
141
|
+
console.log(` Host: ${config.host.padEnd(50)} ${fmt(sources.host)}`);
|
|
142
|
+
console.log(` API Key: ${maskedKey.padEnd(50)} ${fmt(sources.apiKey)}`);
|
|
143
|
+
console.log(` Workspace: ${workspaceLabel.padEnd(50)} ${fmt(sources.workspaceId)}`);
|
|
215
144
|
}
|
|
@@ -9,6 +9,8 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const api_1 = require("../utils/api");
|
|
11
11
|
const config_1 = require("../utils/config");
|
|
12
|
+
const config_write_target_1 = require("../utils/config-write-target");
|
|
13
|
+
const workspace_lookup_1 = require("../utils/workspace-lookup");
|
|
12
14
|
async function workspacesList() {
|
|
13
15
|
const config = (0, api_1.requireConfig)();
|
|
14
16
|
try {
|
|
@@ -52,43 +54,20 @@ async function workspacesList() {
|
|
|
52
54
|
process.exit(1);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
async function workspaceSet(
|
|
57
|
+
async function workspaceSet(input, options = {}) {
|
|
56
58
|
if (process.env.SOLIDACTIONS_WORKSPACE_ID) {
|
|
57
59
|
console.error(chalk_1.default.red('SOLIDACTIONS_WORKSPACE_ID is set in the environment; the change would not take effect. ' +
|
|
58
60
|
'Unset the env var or edit the config file directly.'));
|
|
59
61
|
process.exit(1);
|
|
60
62
|
}
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
const grouped = response.data.workspaces || response.data.data || response.data;
|
|
71
|
-
let allWorkspaces = [];
|
|
72
|
-
if (typeof grouped === 'object' && !Array.isArray(grouped)) {
|
|
73
|
-
for (const orgWorkspaces of Object.values(grouped)) {
|
|
74
|
-
allWorkspaces.push(...orgWorkspaces);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else if (Array.isArray(grouped)) {
|
|
78
|
-
allWorkspaces = grouped;
|
|
79
|
-
}
|
|
80
|
-
const workspace = allWorkspaces.find((w) => w.id === workspaceId || w.slug === workspaceId || w.name === workspaceId);
|
|
81
|
-
if (!workspace) {
|
|
82
|
-
console.error(chalk_1.default.red(`Workspace "${workspaceId}" not found. Run \`solidactions workspace list\` to list available workspaces.`));
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
const updated = { ...config, workspaceId: workspace.id };
|
|
86
|
-
(0, config_1.writeConfigFile)(resolved.activePath, updated);
|
|
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}`));
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
console.error(chalk_1.default.red('Failed to set workspace:'), error.response?.data?.message || error.message);
|
|
92
|
-
process.exit(1);
|
|
63
|
+
const config = (0, api_1.requireResolvedConfig)().config;
|
|
64
|
+
const workspace = await (0, workspace_lookup_1.resolveWorkspaceInput)(config, input);
|
|
65
|
+
const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global });
|
|
66
|
+
const targetPath = (0, config_write_target_1.pathForTarget)(target);
|
|
67
|
+
(0, config_1.writeWorkspaceToFile)(targetPath, workspace.slug ?? workspace.name, workspace.id);
|
|
68
|
+
if (target === 'local') {
|
|
69
|
+
await (0, config_write_target_1.ensureGitignoreCovers)(process.cwd(), !!options.gitignore);
|
|
93
70
|
}
|
|
71
|
+
console.log(chalk_1.default.green(`Workspace set to: ${workspace.name} (${workspace.id})`));
|
|
72
|
+
console.log(chalk_1.default.gray(`Saved to ${targetPath}`));
|
|
94
73
|
}
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const dev_1 = require("./commands/dev");
|
|
|
29
29
|
const ai_init_1 = require("./commands/ai-init");
|
|
30
30
|
const ai_examples_1 = require("./commands/ai-examples");
|
|
31
31
|
const workspaces_1 = require("./commands/workspaces");
|
|
32
|
+
const config_1 = require("./utils/config");
|
|
32
33
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
33
34
|
const pkg = require('../package.json');
|
|
34
35
|
const program = new commander_1.Command();
|
|
@@ -58,6 +59,22 @@ program
|
|
|
58
59
|
.name('solidactions')
|
|
59
60
|
.description('SolidActions CLI - Deploy and manage workflow automation')
|
|
60
61
|
.version(pkg.version);
|
|
62
|
+
program.option('-w, --workspace <id-or-slug-or-name>', 'override active workspace for this command');
|
|
63
|
+
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
64
|
+
const opts = thisCommand.opts();
|
|
65
|
+
const wsOverride = opts.workspace;
|
|
66
|
+
if (wsOverride) {
|
|
67
|
+
// workspace set is a write — -w is for read-paths only.
|
|
68
|
+
const fullName = actionCommand.name();
|
|
69
|
+
const parentName = actionCommand.parent?.name?.();
|
|
70
|
+
const isWorkspaceSet = fullName === 'set' && parentName === 'workspace';
|
|
71
|
+
if (isWorkspaceSet) {
|
|
72
|
+
console.error(chalk_1.default.yellow('warn:') + ' -w/--workspace is ignored on `workspace set`; the positional argument is the new workspace.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
(0, config_1.setCliWorkspaceOverride)(wsOverride);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
61
78
|
// =============================================================================
|
|
62
79
|
// Top-level commands
|
|
63
80
|
// =============================================================================
|
|
@@ -312,10 +329,13 @@ workspace
|
|
|
312
329
|
});
|
|
313
330
|
workspace
|
|
314
331
|
.command('set')
|
|
315
|
-
.description('Set the active workspace
|
|
316
|
-
.argument('<
|
|
317
|
-
.
|
|
318
|
-
(
|
|
332
|
+
.description('Set the active workspace and pin it')
|
|
333
|
+
.argument('<id-or-slug-or-name>', 'Workspace ID, slug, or name')
|
|
334
|
+
.option('--local', 'pin to ./.solidactions/config.json')
|
|
335
|
+
.option('--global', 'pin to ~/.solidactions/config.json')
|
|
336
|
+
.option('--gitignore', 'auto-add .solidactions/ to local .gitignore (skip prompt)')
|
|
337
|
+
.action(async (input, opts) => {
|
|
338
|
+
await (0, workspaces_1.workspaceSet)(input, opts);
|
|
319
339
|
});
|
|
320
340
|
// =============================================================================
|
|
321
341
|
// ai <subcommand>
|
package/dist/utils/api.js
CHANGED
|
@@ -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.augmentNotFoundMessage = augmentNotFoundMessage;
|
|
6
7
|
exports.getApiHeaders = getApiHeaders;
|
|
7
8
|
exports.requireResolvedConfig = requireResolvedConfig;
|
|
8
9
|
exports.requireConfig = requireConfig;
|
|
@@ -12,6 +13,28 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
12
13
|
const chalk_1 = __importDefault(require("chalk"));
|
|
13
14
|
const readline_1 = __importDefault(require("readline"));
|
|
14
15
|
const config_1 = require("./config");
|
|
16
|
+
const workspace_lookup_1 = require("./workspace-lookup");
|
|
17
|
+
// Backend (solidactions-app PR #128) returns: "Project '<slug>' not found in your active workspace '<workspace-slug>'."
|
|
18
|
+
// We require the literal single-quotes around the slug so plausible future error messages
|
|
19
|
+
// like "Project files not found ..." don't false-match.
|
|
20
|
+
const NOT_FOUND_IN_WORKSPACE = /Project '.+' not found in your active workspace/;
|
|
21
|
+
/**
|
|
22
|
+
* Inspect an axios error and, if its response message matches the new
|
|
23
|
+
* workspace-not-found 404 from solidactions-app PR #128, append a
|
|
24
|
+
* remediation hint. Exported for unit testing; the live interceptor
|
|
25
|
+
* below calls it.
|
|
26
|
+
*/
|
|
27
|
+
function augmentNotFoundMessage(error) {
|
|
28
|
+
const msg = error?.response?.data?.message;
|
|
29
|
+
if (typeof msg === 'string'
|
|
30
|
+
&& NOT_FOUND_IN_WORKSPACE.test(msg)
|
|
31
|
+
&& !msg.includes('Did you mean to switch workspaces?')) {
|
|
32
|
+
const hint = "Did you mean to switch workspaces? Run 'solidactions workspace set <name> --local' to pin this directory.";
|
|
33
|
+
error.response.data.message = `${msg}\n\n${hint}`;
|
|
34
|
+
}
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
37
|
+
axios_1.default.interceptors.response.use((response) => response, (error) => Promise.reject(augmentNotFoundMessage(error)));
|
|
15
38
|
function getApiHeaders(config, contentType) {
|
|
16
39
|
const headers = {
|
|
17
40
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
@@ -115,6 +138,15 @@ async function ensureWorkspaceSelected(config) {
|
|
|
115
138
|
return config;
|
|
116
139
|
}
|
|
117
140
|
async function requireConfigWithWorkspace() {
|
|
118
|
-
const
|
|
141
|
+
const resolved = requireResolvedConfig();
|
|
142
|
+
let config = resolved.config;
|
|
143
|
+
// -w override path: source label 'cli' on the workspace field means
|
|
144
|
+
// setCliWorkspaceOverride was called. workspaceId was cleared by resolveConfig
|
|
145
|
+
// because we don't yet know if the input was a slug or UUID. Resolve now.
|
|
146
|
+
if (resolved.sources.workspace === 'cli' && !config.workspaceId) {
|
|
147
|
+
const ws = await (0, workspace_lookup_1.resolveWorkspaceInput)(config, config.workspace);
|
|
148
|
+
config = { ...config, workspace: ws.slug ?? ws.name, workspaceId: ws.id };
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
119
151
|
return ensureWorkspaceSelected(config);
|
|
120
152
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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.decideWriteTarget = decideWriteTarget;
|
|
7
|
+
exports.pathForTarget = pathForTarget;
|
|
8
|
+
exports.ensureGitignoreCovers = ensureGitignoreCovers;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const readline_1 = __importDefault(require("readline"));
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
/**
|
|
15
|
+
* Decide whether a write should target the local or global config file,
|
|
16
|
+
* mirroring the contract used by `login`:
|
|
17
|
+
* - --local / --global mutually exclusive (errors if both set).
|
|
18
|
+
* - If exactly one is set, use it.
|
|
19
|
+
* - Else if TTY, prompt.
|
|
20
|
+
* - Else error and exit.
|
|
21
|
+
*/
|
|
22
|
+
async function decideWriteTarget(options, promptLabel = 'Save config locally (./.solidactions) or globally (~/.solidactions)? [global] ') {
|
|
23
|
+
if (options.local && options.global) {
|
|
24
|
+
console.error(chalk_1.default.red('Error: --local and --global are mutually exclusive.'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (options.local)
|
|
28
|
+
return 'local';
|
|
29
|
+
if (options.global)
|
|
30
|
+
return 'global';
|
|
31
|
+
if (process.stdin.isTTY) {
|
|
32
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
try {
|
|
34
|
+
while (true) {
|
|
35
|
+
const answer = await new Promise((resolve) => rl.question(chalk_1.default.blue(promptLabel), resolve));
|
|
36
|
+
const normalized = answer.trim().toLowerCase();
|
|
37
|
+
if (normalized === '' || normalized === 'global' || normalized === 'g')
|
|
38
|
+
return 'global';
|
|
39
|
+
if (normalized === 'local' || normalized === 'l')
|
|
40
|
+
return 'local';
|
|
41
|
+
console.log(chalk_1.default.yellow("Please answer 'local' or 'global' (or press Enter for global)."));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.error(chalk_1.default.red('Refusing to write config non-interactively. Pass --local or --global.'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
function pathForTarget(target) {
|
|
52
|
+
return target === 'local' ? (0, config_1.getLocalConfigPath)() : (0, config_1.getGlobalConfigPath)();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Ensure `.solidactions/` is in the target directory's `.gitignore`.
|
|
56
|
+
* Idempotent. Skips silently if pattern is already covered.
|
|
57
|
+
*/
|
|
58
|
+
async function ensureGitignoreCovers(targetDir, auto) {
|
|
59
|
+
const gitignorePath = path_1.default.join(targetDir, '.gitignore');
|
|
60
|
+
const patternToAdd = '.solidactions/';
|
|
61
|
+
let existing = '';
|
|
62
|
+
if (fs_1.default.existsSync(gitignorePath)) {
|
|
63
|
+
existing = fs_1.default.readFileSync(gitignorePath, 'utf-8');
|
|
64
|
+
const lines = existing.split('\n').map((l) => l.trim());
|
|
65
|
+
const isCovered = lines.some((line) => {
|
|
66
|
+
const normalized = line
|
|
67
|
+
.replace(/^\*\*\//, '')
|
|
68
|
+
.replace(/^\//, '')
|
|
69
|
+
.replace(/\/(\*\*|\*)?$/, '');
|
|
70
|
+
return normalized === '.solidactions';
|
|
71
|
+
});
|
|
72
|
+
if (isCovered)
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let shouldAdd = auto;
|
|
76
|
+
if (!shouldAdd && process.stdin.isTTY) {
|
|
77
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
78
|
+
let answer;
|
|
79
|
+
try {
|
|
80
|
+
answer = await new Promise((resolve) => {
|
|
81
|
+
rl.question(chalk_1.default.yellow(`Local config directory may contain secrets. Add \`.solidactions/\` to ${gitignorePath}? [Y/n] `), resolve);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rl.close();
|
|
86
|
+
}
|
|
87
|
+
shouldAdd = !(answer.trim().toLowerCase().startsWith('n'));
|
|
88
|
+
}
|
|
89
|
+
if (!shouldAdd) {
|
|
90
|
+
console.log(chalk_1.default.yellow(`Skipping .gitignore update. Remember: ${path_1.default.join(targetDir, '.solidactions', 'config.json')} may contain your API key.`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
94
|
+
try {
|
|
95
|
+
fs_1.default.writeFileSync(gitignorePath, `${existing}${prefix}${patternToAdd}\n`);
|
|
96
|
+
console.log(chalk_1.default.green(`Added \`${patternToAdd}\` to ${gitignorePath}.`));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.log(chalk_1.default.yellow(`Could not update ${gitignorePath}: ${err.message}. Add \`.solidactions/\` to it manually.`));
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -3,38 +3,60 @@ 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.setCliWorkspaceOverride = setCliWorkspaceOverride;
|
|
6
7
|
exports.getGlobalConfigPath = getGlobalConfigPath;
|
|
7
8
|
exports.getLocalConfigPath = getLocalConfigPath;
|
|
8
9
|
exports.findLocalConfigPath = findLocalConfigPath;
|
|
9
10
|
exports.readConfigFile = readConfigFile;
|
|
10
11
|
exports.writeConfigFile = writeConfigFile;
|
|
12
|
+
exports.writeWorkspaceToFile = writeWorkspaceToFile;
|
|
11
13
|
exports.removeConfigFile = removeConfigFile;
|
|
14
|
+
exports.mergeConfigs = mergeConfigs;
|
|
12
15
|
exports.resolveConfig = resolveConfig;
|
|
13
16
|
// src/utils/config.ts
|
|
14
17
|
const fs_1 = __importDefault(require("fs"));
|
|
15
18
|
const os_1 = __importDefault(require("os"));
|
|
16
19
|
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
20
|
const LOCAL_DIR_NAME = '.solidactions';
|
|
20
21
|
const LOCAL_FILE_NAME = 'config.json';
|
|
22
|
+
let cliWorkspaceOverride = undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Set the workspace override from the top-level `-w/--workspace` CLI flag.
|
|
25
|
+
* Module-level state — set once at CLI startup before any subcommand runs.
|
|
26
|
+
* Pass `undefined` to clear (used in tests).
|
|
27
|
+
*/
|
|
28
|
+
function setCliWorkspaceOverride(value) {
|
|
29
|
+
cliWorkspaceOverride = value;
|
|
30
|
+
}
|
|
21
31
|
function getGlobalConfigPath() {
|
|
22
|
-
return
|
|
32
|
+
return path_1.default.join(os_1.default.homedir(), '.solidactions', 'config.json');
|
|
23
33
|
}
|
|
24
34
|
function getLocalConfigPath(cwd = process.cwd()) {
|
|
25
35
|
return path_1.default.join(cwd, LOCAL_DIR_NAME, LOCAL_FILE_NAME);
|
|
26
36
|
}
|
|
27
37
|
/**
|
|
28
38
|
* Walk up from startDir looking for `.solidactions/config.json`.
|
|
29
|
-
* Stops at filesystem root.
|
|
30
|
-
*
|
|
39
|
+
* Stops at filesystem root.
|
|
40
|
+
*
|
|
41
|
+
* Skips both `os.homedir()` (HOME-respecting) and `os.userInfo().homedir`
|
|
42
|
+
* (OS-level real home) so the global config is never matched as a local hit,
|
|
43
|
+
* even when `$HOME` is redirected (test fixtures, sandboxes). In normal runtime
|
|
44
|
+
* the two paths are the same and the Set collapses to one entry.
|
|
45
|
+
*
|
|
31
46
|
* Returns the absolute path of the nearest local config, or null.
|
|
32
47
|
*/
|
|
33
48
|
function findLocalConfigPath(startDir = process.cwd()) {
|
|
34
|
-
const
|
|
49
|
+
const skip = new Set([os_1.default.homedir()]);
|
|
50
|
+
try {
|
|
51
|
+
skip.add(os_1.default.userInfo().homedir);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// os.userInfo() can throw on some platforms (e.g., uid not in /etc/passwd
|
|
55
|
+
// inside containers). Treat it as best-effort — fall back to just os.homedir().
|
|
56
|
+
}
|
|
35
57
|
let dir = path_1.default.resolve(startDir);
|
|
36
58
|
while (true) {
|
|
37
|
-
if (dir
|
|
59
|
+
if (!skip.has(dir)) {
|
|
38
60
|
const candidate = path_1.default.join(dir, LOCAL_DIR_NAME, LOCAL_FILE_NAME);
|
|
39
61
|
if (fs_1.default.existsSync(candidate)) {
|
|
40
62
|
return candidate;
|
|
@@ -80,6 +102,15 @@ function writeConfigFile(filePath, config) {
|
|
|
80
102
|
fs_1.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
81
103
|
fs_1.default.renameSync(tmp, filePath);
|
|
82
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Write a workspace pin to the given config file path. Shallow-merges over
|
|
107
|
+
* any existing keys (preserves host/apiKey if already present). Re-uses
|
|
108
|
+
* writeConfigFile's atomic-write + 0o600 mode contract.
|
|
109
|
+
*/
|
|
110
|
+
function writeWorkspaceToFile(filePath, workspace, workspaceId) {
|
|
111
|
+
const existing = readConfigFile(filePath) ?? {};
|
|
112
|
+
writeConfigFile(filePath, { ...existing, workspace, workspaceId });
|
|
113
|
+
}
|
|
83
114
|
function removeConfigFile(filePath) {
|
|
84
115
|
if (!fs_1.default.existsSync(filePath)) {
|
|
85
116
|
return false;
|
|
@@ -98,26 +129,29 @@ function readEnvOverrides() {
|
|
|
98
129
|
return env;
|
|
99
130
|
}
|
|
100
131
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
132
|
+
* Pure merge of three config layers. Picks each Config field from the highest
|
|
133
|
+
* layer that defines it: env > local > global. Returns null only when no source
|
|
134
|
+
* contributes a host or apiKey (i.e. nothing usable).
|
|
135
|
+
*
|
|
136
|
+
* Invariant the caller must uphold: if `local` is non-null, `localPath` must
|
|
137
|
+
* also be non-null. (`local` is the parsed contents of a file at `localPath`;
|
|
138
|
+
* the path is required for source attribution.) `global` may be null even when
|
|
139
|
+
* `globalPath` is provided — `globalPath` is only used as a source label when
|
|
140
|
+
* `global` contributes a value.
|
|
104
141
|
*/
|
|
105
|
-
function
|
|
106
|
-
const env = readEnvOverrides();
|
|
107
|
-
const localPath = findLocalConfigPath(cwd);
|
|
108
|
-
const local = localPath ? readConfigFile(localPath) : null;
|
|
109
|
-
const global = readConfigFile(getGlobalConfigPath());
|
|
142
|
+
function mergeConfigs(env, local, localPath, global, globalPath) {
|
|
110
143
|
const pick = (key) => {
|
|
111
144
|
if (env[key] !== undefined)
|
|
112
145
|
return { value: env[key], source: 'env' };
|
|
113
146
|
if (local && local[key] !== undefined)
|
|
114
147
|
return { value: local[key], source: localPath };
|
|
115
148
|
if (global && global[key] !== undefined)
|
|
116
|
-
return { value: global[key], source:
|
|
149
|
+
return { value: global[key], source: globalPath };
|
|
117
150
|
return { value: undefined, source: null };
|
|
118
151
|
};
|
|
119
152
|
const host = pick('host');
|
|
120
153
|
const apiKey = pick('apiKey');
|
|
154
|
+
const workspace = pick('workspace');
|
|
121
155
|
const workspaceId = pick('workspaceId');
|
|
122
156
|
if (!host.value && !apiKey.value) {
|
|
123
157
|
return null;
|
|
@@ -126,13 +160,40 @@ function resolveConfig(cwd = process.cwd()) {
|
|
|
126
160
|
config: {
|
|
127
161
|
host: (host.value ?? ''),
|
|
128
162
|
apiKey: (apiKey.value ?? ''),
|
|
163
|
+
workspace: workspace.value,
|
|
129
164
|
workspaceId: workspaceId.value,
|
|
130
165
|
},
|
|
131
166
|
sources: {
|
|
132
167
|
host: host.source,
|
|
133
168
|
apiKey: apiKey.source,
|
|
169
|
+
workspace: workspace.source,
|
|
134
170
|
workspaceId: workspaceId.source,
|
|
135
171
|
},
|
|
136
|
-
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolve config by merging three layers field-by-field (env > local > global).
|
|
176
|
+
* Returns null only when no source contributes an apiKey AND host (i.e. nothing usable).
|
|
177
|
+
* `activePath` is the file a write-mutating command should target: nearest local if present, else global.
|
|
178
|
+
*/
|
|
179
|
+
function resolveConfig(cwd = process.cwd()) {
|
|
180
|
+
const env = readEnvOverrides();
|
|
181
|
+
const localPath = findLocalConfigPath(cwd);
|
|
182
|
+
const local = localPath ? readConfigFile(localPath) : null;
|
|
183
|
+
const globalPath = getGlobalConfigPath();
|
|
184
|
+
const global = readConfigFile(globalPath);
|
|
185
|
+
const merged = mergeConfigs(env, local, localPath, global, globalPath);
|
|
186
|
+
if (!merged)
|
|
187
|
+
return null;
|
|
188
|
+
if (cliWorkspaceOverride !== undefined) {
|
|
189
|
+
merged.config.workspace = cliWorkspaceOverride;
|
|
190
|
+
merged.config.workspaceId = undefined;
|
|
191
|
+
merged.sources.workspace = 'cli';
|
|
192
|
+
merged.sources.workspaceId = 'cli';
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
config: merged.config,
|
|
196
|
+
sources: merged.sources,
|
|
197
|
+
activePath: localPath ?? globalPath,
|
|
137
198
|
};
|
|
138
199
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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.matchWorkspace = matchWorkspace;
|
|
7
|
+
exports.fetchWorkspaces = fetchWorkspaces;
|
|
8
|
+
exports.resolveWorkspaceInput = resolveWorkspaceInput;
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
/**
|
|
12
|
+
* Pure: given a list of workspaces and an input string, return the matching
|
|
13
|
+
* workspace. Match order: id, slug, name (first match wins). Cross-tenant
|
|
14
|
+
* collisions resolve to the first match in the list (pre-existing behavior;
|
|
15
|
+
* see sol-r0b-test-todo.md for follow-up).
|
|
16
|
+
*/
|
|
17
|
+
function matchWorkspace(input, workspaces) {
|
|
18
|
+
return workspaces.find((w) => w.id === input || w.slug === input || w.name === input);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Network: fetch the full list of workspaces the current API key can see.
|
|
22
|
+
* Normalizes the API's grouped/array response shapes.
|
|
23
|
+
*/
|
|
24
|
+
async function fetchWorkspaces(config) {
|
|
25
|
+
const response = await axios_1.default.get(`${config.host}/api/v1/workspaces`, {
|
|
26
|
+
headers: {
|
|
27
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
28
|
+
'Accept': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const grouped = response.data.workspaces || response.data.teams || response.data.data || response.data;
|
|
32
|
+
const out = [];
|
|
33
|
+
if (typeof grouped === 'object' && !Array.isArray(grouped)) {
|
|
34
|
+
for (const orgWorkspaces of Object.values(grouped)) {
|
|
35
|
+
out.push(...orgWorkspaces);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (Array.isArray(grouped)) {
|
|
39
|
+
out.push(...grouped);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* High-level: fetch + match. Errors with a clear CLI message and exits
|
|
45
|
+
* non-zero if no match is found.
|
|
46
|
+
*/
|
|
47
|
+
async function resolveWorkspaceInput(config, input) {
|
|
48
|
+
let workspaces;
|
|
49
|
+
try {
|
|
50
|
+
workspaces = await fetchWorkspaces(config);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error(chalk_1.default.red('Failed to list workspaces:'), error.response?.data?.message || error.message);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const match = matchWorkspace(input, workspaces);
|
|
57
|
+
if (!match) {
|
|
58
|
+
console.error(chalk_1.default.red(`Workspace "${input}" not found. Run \`solidactions workspace list\` to list available workspaces.`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
return match;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,59 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
2
|
+
"name": "@solidactions/cli",
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "SolidActions CLI - Deploy and manage workflow automation",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"solidactions": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/SolidActions/solidactions-cli.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/SolidActions/solidactions-cli",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/SolidActions/solidactions-cli/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"solidactions",
|
|
29
|
+
"cli",
|
|
30
|
+
"workflow",
|
|
31
|
+
"automation",
|
|
32
|
+
"deploy"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"archiver": "^7.0.0",
|
|
42
|
+
"axios": "^1.6.0",
|
|
43
|
+
"chalk": "^4.1.2",
|
|
44
|
+
"commander": "^11.0.0",
|
|
45
|
+
"dotenv": "^17.3.1",
|
|
46
|
+
"form-data": "^4.0.0",
|
|
47
|
+
"fs-extra": "^11.0.0",
|
|
48
|
+
"js-yaml": "^4.1.0",
|
|
49
|
+
"prompts": "^2.4.2",
|
|
50
|
+
"tar": "^7.5.12"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/archiver": "^6.0.0",
|
|
54
|
+
"@types/fs-extra": "^11.0.0",
|
|
55
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
|
+
"@types/node": "^20.0.0",
|
|
57
|
+
"@types/prompts": "^2.4.9",
|
|
58
|
+
"@types/tar": "^6.1.13",
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
"vitest": "^4.1.5"
|
|
61
|
+
}
|
|
59
62
|
}
|