@solidactions/cli 1.23.1 → 1.25.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 +2 -2
- package/dist/commands/ai-init.js +44 -29
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/dev.js +25 -2
- package/dist/commands/docs-push.js +1 -1
- package/dist/commands/env-delete.js +18 -10
- package/dist/commands/env-list.js +40 -29
- package/dist/commands/env-pull.js +6 -4
- package/dist/commands/env-push.js +4 -4
- package/dist/commands/env-set.js +12 -3
- package/dist/commands/init.js +18 -1
- package/dist/commands/login.js +76 -20
- package/dist/commands/oauth-actions-list.js +5 -2
- package/dist/commands/oauth-actions-platforms.js +41 -0
- package/dist/commands/oauth-actions-search.js +5 -2
- package/dist/commands/project-list.js +19 -13
- package/dist/commands/pull.js +16 -1
- package/dist/commands/role-push.js +3 -3
- package/dist/commands/run-list.js +24 -12
- package/dist/commands/run-start.js +7 -1
- package/dist/commands/run-view.js +5 -0
- package/dist/commands/skill-pull.js +1 -1
- package/dist/commands/skill-push.js +3 -3
- package/dist/index.js +38 -17
- package/dist/utils/api.js +70 -1
- package/dist/utils/config-write-target.js +16 -0
- package/dist/utils/config.js +17 -2
- package/dist/utils/env.js +15 -3
- package/dist/utils/mcp.js +15 -5
- package/dist/utils/table.js +62 -0
- package/package.json +1 -1
package/dist/utils/api.js
CHANGED
|
@@ -5,8 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.augmentNotFoundMessage = augmentNotFoundMessage;
|
|
7
7
|
exports.getApiHeaders = getApiHeaders;
|
|
8
|
+
exports.describeProjectEnvironments = describeProjectEnvironments;
|
|
9
|
+
exports.formatValidationError = formatValidationError;
|
|
8
10
|
exports.requireResolvedConfig = requireResolvedConfig;
|
|
9
11
|
exports.requireConfig = requireConfig;
|
|
12
|
+
exports.authFailureMessage = authFailureMessage;
|
|
10
13
|
exports.ensureWorkspaceSelected = ensureWorkspaceSelected;
|
|
11
14
|
exports.requireConfigWithWorkspace = requireConfigWithWorkspace;
|
|
12
15
|
const axios_1 = __importDefault(require("axios"));
|
|
@@ -46,6 +49,59 @@ function getApiHeaders(config, contentType) {
|
|
|
46
49
|
headers['X-Workspace-Id'] = config.workspaceId;
|
|
47
50
|
return headers;
|
|
48
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort lookup of the environments a project family actually has
|
|
54
|
+
* (e.g. "production, dev"), for a friendlier 404 message. Returns null on
|
|
55
|
+
* any failure — callers should treat that as "no extra detail available."
|
|
56
|
+
*/
|
|
57
|
+
async function describeProjectEnvironments(config, projectName) {
|
|
58
|
+
try {
|
|
59
|
+
const res = await axios_1.default.get(`${config.host}/api/v1/projects`, { headers: getApiHeaders(config) });
|
|
60
|
+
const rows = res.data?.data ?? res.data ?? [];
|
|
61
|
+
const hit = rows.find((p) => p.name === projectName || p.slug === projectName);
|
|
62
|
+
const envs = hit?.environments;
|
|
63
|
+
return envs?.length ? envs.join(', ') : null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Render a Laravel-style 422 validation error body as plain readable text —
|
|
71
|
+
* never a raw JSON dump. Prefers `data.errors` (flattened, one message per
|
|
72
|
+
* line, with the internal `variables.N.` index prefix stripped and the bare
|
|
73
|
+
* `key` attribute renamed to `variable key` for clarity); falls back to
|
|
74
|
+
* `data.message`.
|
|
75
|
+
*/
|
|
76
|
+
function formatValidationError(data) {
|
|
77
|
+
const errors = data?.errors;
|
|
78
|
+
let messages = [];
|
|
79
|
+
if (errors && typeof errors === 'object' && !Array.isArray(errors)) {
|
|
80
|
+
for (const value of Object.values(errors)) {
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
messages.push(...value.map((v) => String(v)));
|
|
83
|
+
}
|
|
84
|
+
else if (value) {
|
|
85
|
+
messages.push(String(value));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (messages.length === 0) {
|
|
90
|
+
const message = data?.message;
|
|
91
|
+
if (typeof message === 'string' && message) {
|
|
92
|
+
messages = [message];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (messages.length === 0) {
|
|
96
|
+
return 'Validation failed.';
|
|
97
|
+
}
|
|
98
|
+
return messages
|
|
99
|
+
.map((msg) => msg
|
|
100
|
+
.replace(/variables\.\d+\.key/gi, 'variable key')
|
|
101
|
+
.replace(/variable key field/gi, 'variable key')
|
|
102
|
+
.replace(/variables\.\d+\.(\w+)/gi, '$1'))
|
|
103
|
+
.join('\n');
|
|
104
|
+
}
|
|
49
105
|
/**
|
|
50
106
|
* Get the full resolution (config + sources + activePath). Exits if nothing resolvable.
|
|
51
107
|
*/
|
|
@@ -60,6 +116,15 @@ function requireResolvedConfig() {
|
|
|
60
116
|
function requireConfig() {
|
|
61
117
|
return requireResolvedConfig().config;
|
|
62
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Contextual 401 message — names the host being called and where the (now
|
|
121
|
+
* apparently invalid/expired) API key came from, instead of a bare
|
|
122
|
+
* "Authentication failed" that gives no clue which config is at fault.
|
|
123
|
+
*/
|
|
124
|
+
function authFailureMessage(config, sources) {
|
|
125
|
+
const keySource = sources?.apiKey ?? 'config';
|
|
126
|
+
return `Authentication failed against ${config.host} (key from ${keySource}). Run \`solidactions login <api-key>\` to re-configure.`;
|
|
127
|
+
}
|
|
63
128
|
async function ensureWorkspaceSelected(config) {
|
|
64
129
|
if (config.workspaceId) {
|
|
65
130
|
return config;
|
|
@@ -95,7 +160,7 @@ async function ensureWorkspaceSelected(config) {
|
|
|
95
160
|
}
|
|
96
161
|
catch (error) {
|
|
97
162
|
if (error.response?.status === 401) {
|
|
98
|
-
console.error(chalk_1.default.red(
|
|
163
|
+
console.error(chalk_1.default.red(authFailureMessage(config, resolved?.sources ?? null)));
|
|
99
164
|
}
|
|
100
165
|
else {
|
|
101
166
|
console.error(chalk_1.default.red('Failed to fetch workspaces:'), error.response?.data?.message || error.message);
|
|
@@ -148,5 +213,9 @@ async function requireConfigWithWorkspace() {
|
|
|
148
213
|
config = { ...config, workspace: ws.slug ?? ws.name, workspaceId: ws.id };
|
|
149
214
|
return config;
|
|
150
215
|
}
|
|
216
|
+
if (!config.workspaceId && !process.stdin.isTTY) {
|
|
217
|
+
console.error(chalk_1.default.red('No workspace set for this config. Run `solidactions workspace set <name-or-id> --local` (or --global).'));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
151
220
|
return ensureWorkspaceSelected(config);
|
|
152
221
|
}
|
|
@@ -78,7 +78,23 @@ async function confirmOverwrite(existingPath, backupPath) {
|
|
|
78
78
|
* Ensure `.solidactions/` is in the target directory's `.gitignore`.
|
|
79
79
|
* Idempotent. Skips silently if pattern is already covered.
|
|
80
80
|
*/
|
|
81
|
+
/** Walk up from startDir looking for a `.git` directory. Stops at filesystem root. */
|
|
82
|
+
function findGitRoot(startDir) {
|
|
83
|
+
let dir = path_1.default.resolve(startDir);
|
|
84
|
+
while (true) {
|
|
85
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, '.git'))) {
|
|
86
|
+
return dir;
|
|
87
|
+
}
|
|
88
|
+
const parent = path_1.default.dirname(dir);
|
|
89
|
+
if (parent === dir)
|
|
90
|
+
return null;
|
|
91
|
+
dir = parent;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
81
94
|
async function ensureGitignoreCovers(targetDir, auto) {
|
|
95
|
+
// Not in a git repo — a .gitignore entry protects nothing here; skip silently.
|
|
96
|
+
if (!findGitRoot(targetDir))
|
|
97
|
+
return;
|
|
82
98
|
const gitignorePath = path_1.default.join(targetDir, '.gitignore');
|
|
83
99
|
const patternToAdd = '.solidactions/';
|
|
84
100
|
let existing = '';
|
package/dist/utils/config.js
CHANGED
|
@@ -151,8 +151,23 @@ function mergeConfigs(env, local, localPath, global, globalPath) {
|
|
|
151
151
|
};
|
|
152
152
|
const host = pick('host');
|
|
153
153
|
const apiKey = pick('apiKey');
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// A local config that defines its own credentials (host or apiKey) is
|
|
155
|
+
// anchored to a different account/tenant than the global config — its
|
|
156
|
+
// workspace/workspaceId must never fall back to global (F-C3).
|
|
157
|
+
// Pure workspace-pin local files (no host/apiKey of their own) keep
|
|
158
|
+
// inheriting global credentials and so may still inherit the workspace.
|
|
159
|
+
const localDefinesCreds = !!(local && (local.host !== undefined || local.apiKey !== undefined));
|
|
160
|
+
const pickWorkspaceField = (key) => {
|
|
161
|
+
if (env[key] !== undefined)
|
|
162
|
+
return { value: env[key], source: 'env' };
|
|
163
|
+
if (local && local[key] !== undefined)
|
|
164
|
+
return { value: local[key], source: localPath };
|
|
165
|
+
if (!localDefinesCreds && global && global[key] !== undefined)
|
|
166
|
+
return { value: global[key], source: globalPath };
|
|
167
|
+
return { value: undefined, source: null };
|
|
168
|
+
};
|
|
169
|
+
const workspace = pickWorkspaceField('workspace');
|
|
170
|
+
const workspaceId = pickWorkspaceField('workspaceId');
|
|
156
171
|
if (!host.value && !apiKey.value) {
|
|
157
172
|
return null;
|
|
158
173
|
}
|
package/dist/utils/env.js
CHANGED
|
@@ -10,6 +10,8 @@ exports.getYamlDeclaredVars = getYamlDeclaredVars;
|
|
|
10
10
|
exports.loadSolidActionsConfig = loadSolidActionsConfig;
|
|
11
11
|
exports.isReservedEnvName = isReservedEnvName;
|
|
12
12
|
exports.reservedEnvNameError = reservedEnvNameError;
|
|
13
|
+
exports.isValidEnvName = isValidEnvName;
|
|
14
|
+
exports.envNameError = envNameError;
|
|
13
15
|
const fs_1 = __importDefault(require("fs"));
|
|
14
16
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
15
17
|
/**
|
|
@@ -81,7 +83,7 @@ function parseYamlEnvVars(config) {
|
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
83
85
|
* Extract declared variable keys from solidactions.yaml env config.
|
|
84
|
-
* Returns a set of
|
|
86
|
+
* Returns a set of variable keys that are declared in YAML.
|
|
85
87
|
*/
|
|
86
88
|
function getYamlDeclaredVars(config) {
|
|
87
89
|
const parsedVars = parseYamlEnvVars(config);
|
|
@@ -103,10 +105,20 @@ function loadSolidActionsConfig(yamlPath) {
|
|
|
103
105
|
* matching the server-side rule (App\Support\ReservedEnvNames).
|
|
104
106
|
*/
|
|
105
107
|
exports.RESERVED_ENV_PREFIX = 'SOLIDACTIONS_';
|
|
108
|
+
/** Case-insensitive: the server rejects any casing of the prefix, not just uppercase. */
|
|
106
109
|
function isReservedEnvName(key) {
|
|
107
|
-
return key.startsWith(exports.RESERVED_ENV_PREFIX);
|
|
110
|
+
return key.toUpperCase().startsWith(exports.RESERVED_ENV_PREFIX);
|
|
108
111
|
}
|
|
109
112
|
function reservedEnvNameError(key) {
|
|
110
|
-
const suggestion = key.replace(/^SOLIDACTIONS_
|
|
113
|
+
const suggestion = key.replace(/^SOLIDACTIONS_/i, 'MY_');
|
|
111
114
|
return `"${key}" uses the reserved ${exports.RESERVED_ENV_PREFIX} prefix. These names are set by the platform at runtime (API credentials, run context) and a custom variable would clobber them, causing authentication failures. Choose a different name (e.g. "${suggestion}").`;
|
|
112
115
|
}
|
|
116
|
+
/** Env/variable key naming rule: letters, digits, underscore; no leading digit. */
|
|
117
|
+
function isValidEnvName(key) {
|
|
118
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
119
|
+
}
|
|
120
|
+
function envNameError(key) {
|
|
121
|
+
if (key.trim() === '')
|
|
122
|
+
return 'Variable key is required.';
|
|
123
|
+
return `Invalid variable name "${key}" — names must match [A-Za-z_][A-Za-z0-9_]* (letters, digits, underscore; no leading digit).`;
|
|
124
|
+
}
|
package/dist/utils/mcp.js
CHANGED
|
@@ -46,6 +46,9 @@ const http = __importStar(require("http"));
|
|
|
46
46
|
const https = __importStar(require("https"));
|
|
47
47
|
const url_1 = require("url");
|
|
48
48
|
const api_1 = require("./api");
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
50
|
+
const pkg = require('../../package.json');
|
|
51
|
+
const CLI_VERSION = pkg.version;
|
|
49
52
|
/**
|
|
50
53
|
* Internal: call a single MCP tool on the given endpoint path.
|
|
51
54
|
*
|
|
@@ -85,7 +88,10 @@ async function callMcpTool(config, endpointPath, toolName, args) {
|
|
|
85
88
|
let raw = '';
|
|
86
89
|
res.on('data', (chunk) => { raw += chunk; });
|
|
87
90
|
res.on('end', () => {
|
|
88
|
-
if (res.statusCode
|
|
91
|
+
if (res.statusCode === 404) {
|
|
92
|
+
reject(new Error(`MCP request failed: ${parsed.host} has no ${endpointPath} endpoint — the server may be older or newer than this CLI (${CLI_VERSION}). Raw: HTTP 404 ${raw}`));
|
|
93
|
+
}
|
|
94
|
+
else if (res.statusCode && res.statusCode >= 400) {
|
|
89
95
|
reject(new Error(`MCP request failed with HTTP ${res.statusCode}: ${raw}`));
|
|
90
96
|
}
|
|
91
97
|
else {
|
|
@@ -116,15 +122,19 @@ async function callMcpTool(config, endpointPath, toolName, args) {
|
|
|
116
122
|
}
|
|
117
123
|
return { ok: !isError, data: toolData };
|
|
118
124
|
}
|
|
125
|
+
const UNIFIED_MCP_PATH = '/mcp';
|
|
126
|
+
// Server consolidated per-domain MCP servers into one endpoint with namespaced tools.
|
|
127
|
+
const CREWS_TOOL_NAMES = { skills: 'crews_skills', roles: 'crews_roles' };
|
|
119
128
|
/**
|
|
120
|
-
* Call a single MCP tool on /mcp
|
|
129
|
+
* Call a single MCP tool on the unified /mcp endpoint, mapping legacy crews
|
|
130
|
+
* tool names ('skills'/'roles') to their namespaced equivalents.
|
|
121
131
|
*/
|
|
122
132
|
async function callCrewsTool(config, toolName, args) {
|
|
123
|
-
return callMcpTool(config,
|
|
133
|
+
return callMcpTool(config, UNIFIED_MCP_PATH, CREWS_TOOL_NAMES[toolName] ?? toolName, args);
|
|
124
134
|
}
|
|
125
135
|
/**
|
|
126
|
-
* Call a single MCP tool on /mcp
|
|
136
|
+
* Call a single MCP tool on the unified /mcp endpoint.
|
|
127
137
|
*/
|
|
128
138
|
async function callDocsTool(config, toolName, args) {
|
|
129
|
-
return callMcpTool(config,
|
|
139
|
+
return callMcpTool(config, UNIFIED_MCP_PATH, toolName, args);
|
|
130
140
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/utils/table.ts
|
|
3
|
+
//
|
|
4
|
+
// Shared plain-text table renderer (extracted from webhook-formatters.ts's
|
|
5
|
+
// columnWidth helper) used by project-list, env-list, and run-list.
|
|
6
|
+
//
|
|
7
|
+
// Operates on PLAIN (uncolored) cell values only — chalk ANSI escape codes
|
|
8
|
+
// count toward a JS string's .length, so colorizing a cell before padding
|
|
9
|
+
// would corrupt the alignment math. Callers that want color should apply it
|
|
10
|
+
// to the whole rendered line (e.g. chalk.gray(lines[i])), not to individual
|
|
11
|
+
// cell values passed in here.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.sanitizeCell = sanitizeCell;
|
|
14
|
+
exports.truncateCell = truncateCell;
|
|
15
|
+
exports.computeColumnWidths = computeColumnWidths;
|
|
16
|
+
exports.renderTable = renderTable;
|
|
17
|
+
const DEFAULT_MAX_WIDTH = 60;
|
|
18
|
+
/** Escape embedded newlines so a row can never span multiple terminal lines. */
|
|
19
|
+
function sanitizeCell(value) {
|
|
20
|
+
return value.replace(/\r?\n/g, '\\n');
|
|
21
|
+
}
|
|
22
|
+
function truncateCell(value, maxWidth = DEFAULT_MAX_WIDTH) {
|
|
23
|
+
if (value.length <= maxWidth)
|
|
24
|
+
return value;
|
|
25
|
+
return value.slice(0, Math.max(0, maxWidth - 1)) + '…';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compute per-column widths (`max(minWidth, longest sanitized+truncated cell) + 2`)
|
|
29
|
+
* without rendering — for callers (e.g. run-list) that need to colorize a
|
|
30
|
+
* cell's text with chalk AFTER padding (coloring first would corrupt the
|
|
31
|
+
* alignment math, since ANSI escape codes count toward a JS string's .length).
|
|
32
|
+
*/
|
|
33
|
+
function computeColumnWidths(headers, rows, opts = {}) {
|
|
34
|
+
const maxWidth = opts.maxWidth ?? DEFAULT_MAX_WIDTH;
|
|
35
|
+
const minWidths = opts.minWidths ?? [];
|
|
36
|
+
const clean = (v) => truncateCell(sanitizeCell(v), maxWidth);
|
|
37
|
+
return headers.map((h, i) => {
|
|
38
|
+
const longest = Math.max(clean(h).length, ...rows.map((r) => clean(r[i] ?? '').length));
|
|
39
|
+
return Math.max(minWidths[i] ?? 0, longest) + 2;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Render a simple aligned table as an array of lines: `[header, divider, ...rows]`.
|
|
44
|
+
* Column widths are dynamic — `max(minWidth, longest cell in that column) + 2`.
|
|
45
|
+
*/
|
|
46
|
+
function renderTable(headers, rows, opts = {}) {
|
|
47
|
+
const maxWidth = opts.maxWidth ?? DEFAULT_MAX_WIDTH;
|
|
48
|
+
const minWidths = opts.minWidths ?? [];
|
|
49
|
+
const cleanHeaders = headers.map((h) => truncateCell(sanitizeCell(h), maxWidth));
|
|
50
|
+
const cleanRows = rows.map((row) => row.map((cell) => truncateCell(sanitizeCell(cell), maxWidth)));
|
|
51
|
+
const widths = cleanHeaders.map((h, i) => {
|
|
52
|
+
const longest = Math.max(h.length, ...cleanRows.map((r) => r[i]?.length ?? 0));
|
|
53
|
+
return Math.max(minWidths[i] ?? 0, longest) + 2;
|
|
54
|
+
});
|
|
55
|
+
const renderRow = (cells) => cells.map((cell, i) => (i === cells.length - 1 ? cell : (cell ?? '').padEnd(widths[i]))).join('');
|
|
56
|
+
const totalWidth = widths.reduce((a, b) => a + b, 0);
|
|
57
|
+
return [
|
|
58
|
+
renderRow(cleanHeaders),
|
|
59
|
+
'-'.repeat(totalWidth),
|
|
60
|
+
...cleanRows.map(renderRow),
|
|
61
|
+
];
|
|
62
|
+
}
|