@solidactions/cli 1.23.1 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1 -1
- package/dist/commands/oauth-actions-search.js +1 -1
- 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 +29 -16
- 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/commands/login.js
CHANGED
|
@@ -13,11 +13,11 @@ exports.login = login;
|
|
|
13
13
|
exports.logout = logout;
|
|
14
14
|
exports.whoami = whoami;
|
|
15
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const readline_1 = __importDefault(require("readline"));
|
|
16
17
|
const chalk_1 = __importDefault(require("chalk"));
|
|
17
|
-
const api_1 = require("../utils/api");
|
|
18
|
-
const workspaces_1 = require("./workspaces");
|
|
19
18
|
const config_1 = require("../utils/config");
|
|
20
19
|
const config_write_target_1 = require("../utils/config-write-target");
|
|
20
|
+
const workspace_lookup_1 = require("../utils/workspace-lookup");
|
|
21
21
|
function getConfig() {
|
|
22
22
|
const resolved = (0, config_1.resolveConfig)();
|
|
23
23
|
return resolved ? resolved.config : null;
|
|
@@ -60,6 +60,36 @@ function loginHostLines(resolved) {
|
|
|
60
60
|
}
|
|
61
61
|
return [`Host: ${resolved.host}`];
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Prompt the user to pick a workspace from an already-fetched list. Auto-
|
|
65
|
+
* selects when there's exactly one. Returns undefined if none exist.
|
|
66
|
+
*/
|
|
67
|
+
async function selectWorkspaceInteractively(workspaces) {
|
|
68
|
+
if (workspaces.length === 0) {
|
|
69
|
+
console.log(chalk_1.default.yellow('No workspaces found. Create one at your SolidActions dashboard, then run `solidactions workspace set <name>`.'));
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (workspaces.length === 1) {
|
|
73
|
+
console.log(chalk_1.default.gray(`Auto-selected workspace: ${workspaces[0].name}`));
|
|
74
|
+
return workspaces[0];
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk_1.default.blue('\nSelect a workspace:\n'));
|
|
77
|
+
workspaces.forEach((ws, i) => {
|
|
78
|
+
console.log(` ${chalk_1.default.white(`${i + 1}.`)} ${ws.name}`);
|
|
79
|
+
});
|
|
80
|
+
console.log('');
|
|
81
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
+
const answer = await new Promise((resolve) => {
|
|
83
|
+
rl.question(chalk_1.default.blue('Enter number: '), resolve);
|
|
84
|
+
});
|
|
85
|
+
rl.close();
|
|
86
|
+
const index = parseInt(answer, 10) - 1;
|
|
87
|
+
if (isNaN(index) || index < 0 || index >= workspaces.length) {
|
|
88
|
+
console.error(chalk_1.default.red('Invalid selection.'));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return workspaces[index];
|
|
92
|
+
}
|
|
63
93
|
async function login(apiKey, options) {
|
|
64
94
|
const resolved = resolveLoginHost(options);
|
|
65
95
|
const host = resolved.host;
|
|
@@ -68,9 +98,6 @@ async function login(apiKey, options) {
|
|
|
68
98
|
console.log(chalk_1.default.gray('Generate an API key at: ') + chalk_1.default.blue(`${host}/settings/api-keys`));
|
|
69
99
|
process.exit(1);
|
|
70
100
|
}
|
|
71
|
-
// Determine target location.
|
|
72
|
-
const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global }, undefined, LOGIN_REFUSAL_MESSAGE);
|
|
73
|
-
const targetPath = (0, config_write_target_1.pathForTarget)(target);
|
|
74
101
|
console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
|
|
75
102
|
for (const line of loginHostLines(resolved)) {
|
|
76
103
|
console.log(resolved.isDefault ? chalk_1.default.yellow(line) : chalk_1.default.gray(line));
|
|
@@ -79,6 +106,45 @@ async function login(apiKey, options) {
|
|
|
79
106
|
host,
|
|
80
107
|
apiKey: apiKey.trim(),
|
|
81
108
|
};
|
|
109
|
+
// 1. Validate the key BEFORE any disk write.
|
|
110
|
+
let workspaces;
|
|
111
|
+
try {
|
|
112
|
+
workspaces = await (0, workspace_lookup_1.fetchWorkspaces)(config);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
if (e.response?.status === 401) {
|
|
116
|
+
console.error(chalk_1.default.red(`Invalid API key for ${host}.`));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.error(chalk_1.default.red(`Could not reach ${host}: ${e.message}`));
|
|
120
|
+
}
|
|
121
|
+
process.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// 2. Resolve the workspace (still before writing).
|
|
125
|
+
if (options.workspace) {
|
|
126
|
+
const match = (0, workspace_lookup_1.matchWorkspace)(options.workspace, workspaces);
|
|
127
|
+
if (!match) {
|
|
128
|
+
console.error(chalk_1.default.red(`Workspace "${options.workspace}" not found. Run \`solidactions workspace list\` to list available workspaces.`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
config.workspace = match.slug ?? match.name;
|
|
133
|
+
config.workspaceId = match.id;
|
|
134
|
+
}
|
|
135
|
+
else if (process.stdin.isTTY) {
|
|
136
|
+
const selected = await selectWorkspaceInteractively(workspaces);
|
|
137
|
+
if (selected) {
|
|
138
|
+
config.workspace = selected.slug ?? selected.name;
|
|
139
|
+
config.workspaceId = selected.id;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(chalk_1.default.yellow('No workspace set — run `solidactions workspace set <name>` later.'));
|
|
144
|
+
}
|
|
145
|
+
// 3. Determine target location, back up if needed, and write ONCE.
|
|
146
|
+
const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global }, undefined, LOGIN_REFUSAL_MESSAGE);
|
|
147
|
+
const targetPath = (0, config_write_target_1.pathForTarget)(target);
|
|
82
148
|
const existingRaw = fs_1.default.existsSync(targetPath) ? fs_1.default.readFileSync(targetPath, 'utf-8') : null;
|
|
83
149
|
const wouldChange = existingRaw !== null && existingRaw.trim() !== JSON.stringify(config, null, 2).trim();
|
|
84
150
|
if (wouldChange) {
|
|
@@ -98,19 +164,6 @@ async function login(apiKey, options) {
|
|
|
98
164
|
console.log(chalk_1.default.green('Logged in successfully!'));
|
|
99
165
|
console.log(chalk_1.default.gray(`Configuration saved to ${targetPath}`));
|
|
100
166
|
console.log('');
|
|
101
|
-
// Workspace selection — ensureWorkspaceSelected writes to the right file via the resolver.
|
|
102
|
-
try {
|
|
103
|
-
if (options.workspace) {
|
|
104
|
-
await (0, workspaces_1.workspaceSet)(options.workspace);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
await (0, api_1.ensureWorkspaceSelected)(config);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
console.log(chalk_1.default.yellow('Could not set workspace. Run `solidactions workspace set` later.'));
|
|
112
|
-
}
|
|
113
|
-
console.log('');
|
|
114
167
|
console.log(chalk_1.default.blue('Next step — scaffold a new project (includes AI tooling):'));
|
|
115
168
|
console.log(chalk_1.default.gray(' solidactions init <project-name> Creates ./<project-name>/ with scaffold + AI skills'));
|
|
116
169
|
console.log(chalk_1.default.gray(' solidactions init Scaffolds in the current (empty) directory'));
|
|
@@ -146,7 +199,8 @@ function logout(options = {}) {
|
|
|
146
199
|
console.log(chalk_1.default.green(`Logged out. Removed ${targetPath}`));
|
|
147
200
|
}
|
|
148
201
|
else {
|
|
149
|
-
console.log(chalk_1.default.gray(`Not logged in (no config at ${targetPath}).`));
|
|
202
|
+
console.log(chalk_1.default.gray(`Not logged in (no config at ${targetPath}) — nothing to remove.`));
|
|
203
|
+
process.exit(0);
|
|
150
204
|
}
|
|
151
205
|
}
|
|
152
206
|
function whoami() {
|
|
@@ -174,8 +228,10 @@ function whoami() {
|
|
|
174
228
|
: config.workspaceId
|
|
175
229
|
? `${config.workspaceId} (slug unknown — run 'workspace set <slug>' to populate)`
|
|
176
230
|
: '';
|
|
231
|
+
const isFileSource = (src) => src !== null && src !== 'env' && src !== 'cli';
|
|
232
|
+
const workspaceInheritedFromOtherFile = isFileSource(sources.workspaceId) && sources.workspaceId !== sources.apiKey;
|
|
177
233
|
console.log(chalk_1.default.blue('Current configuration:'));
|
|
178
234
|
console.log(` Host: ${config.host.padEnd(50)} ${fmt(sources.host)}`);
|
|
179
235
|
console.log(` API Key: ${maskedKey.padEnd(50)} ${fmt(sources.apiKey)}`);
|
|
180
|
-
console.log(` Workspace: ${workspaceLabel.padEnd(50)} ${fmt(sources.workspaceId)}`);
|
|
236
|
+
console.log(` Workspace: ${workspaceLabel.padEnd(50)} ${fmt(sources.workspaceId)}${workspaceInheritedFromOtherFile ? chalk_1.default.yellow(' (inherited from a different config file)') : ''}`);
|
|
181
237
|
}
|
|
@@ -23,7 +23,7 @@ async function oauthActionsList(platform, options) {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (actions.length === 0) {
|
|
26
|
-
console.log(
|
|
26
|
+
console.log(`No actions found for "${platform}" — check the platform name and that a connection exists (\`solidactions env pull --update-oauth\` syncs connections).`);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
for (const a of actions) {
|
|
@@ -27,7 +27,7 @@ async function oauthActionsSearch(platform, query, options) {
|
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
if (actions.length === 0) {
|
|
30
|
-
console.log(
|
|
30
|
+
console.log(`No actions found for "${platform}" — check the platform name and that a connection exists (\`solidactions env pull --update-oauth\` syncs connections).`);
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
for (const a of actions) {
|
|
@@ -7,30 +7,36 @@ exports.projectList = projectList;
|
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const api_1 = require("../utils/api");
|
|
10
|
-
|
|
10
|
+
const table_1 = require("../utils/table");
|
|
11
|
+
async function projectList(options = {}) {
|
|
11
12
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
12
|
-
console.log(chalk_1.default.blue('Projects:\n'));
|
|
13
13
|
try {
|
|
14
14
|
const response = await axios_1.default.get(`${config.host}/api/v1/projects`, {
|
|
15
15
|
headers: (0, api_1.getApiHeaders)(config),
|
|
16
16
|
});
|
|
17
17
|
const projects = response.data.data || [];
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Header is only printed after the fetch succeeds — an auth/network
|
|
23
|
+
// failure used to print "Projects:" followed immediately by the error.
|
|
24
|
+
console.log(chalk_1.default.blue('Projects:\n'));
|
|
18
25
|
if (projects.length === 0) {
|
|
19
26
|
console.log(chalk_1.default.gray('No projects found.'));
|
|
20
27
|
return;
|
|
21
28
|
}
|
|
22
|
-
|
|
23
|
-
console.log(chalk_1.default.gray('-'.repeat(100)));
|
|
24
|
-
for (const project of projects) {
|
|
25
|
-
const name = (project.name || '?').padEnd(25);
|
|
26
|
-
const status = project.status || '?';
|
|
27
|
-
const statusColor = status === 'ready' ? chalk_1.default.green : status === 'building' ? chalk_1.default.yellow : chalk_1.default.gray;
|
|
28
|
-
const snapshot = (project.snapshot_name || '-').padEnd(30);
|
|
29
|
+
const rows = projects.map((project) => {
|
|
29
30
|
const envs = (project.environments || [project.environment || 'production']).join(', ');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
return [project.name || '?', project.status || '?', project.snapshot_name || '-', envs];
|
|
32
|
+
});
|
|
33
|
+
const lines = (0, table_1.renderTable)(['NAME', 'STATUS', 'SNAPSHOT', 'ENVIRONMENTS'], rows, {
|
|
34
|
+
minWidths: [25, 15, 30],
|
|
35
|
+
});
|
|
36
|
+
console.log(chalk_1.default.gray(lines[0]));
|
|
37
|
+
console.log(chalk_1.default.gray(lines[1]));
|
|
38
|
+
for (const line of lines.slice(2)) {
|
|
39
|
+
console.log(line);
|
|
34
40
|
}
|
|
35
41
|
console.log('');
|
|
36
42
|
console.log(chalk_1.default.gray(`${projects.length} project(s)`));
|
package/dist/commands/pull.js
CHANGED
|
@@ -45,8 +45,23 @@ async function pull(projectName, destPath, options = {}) {
|
|
|
45
45
|
console.log(chalk_1.default.gray(`Downloaded ${buffer.length} bytes`));
|
|
46
46
|
console.log(chalk_1.default.yellow(`Extracting to ${destination}...`));
|
|
47
47
|
fs_1.default.mkdirSync(destination, { recursive: true });
|
|
48
|
+
// The server streams back the exact archive `deploy` uploaded: a
|
|
49
|
+
// root-level Dockerfile plus every project file nested under
|
|
50
|
+
// tenantcode/ (see deploy.ts:510-527). Unwrap it here so the pulled
|
|
51
|
+
// project's files land at dest root, matching what the user actually
|
|
52
|
+
// wrote — dropping the generated Dockerfile and any noCache
|
|
53
|
+
// cache-buster entries (deploy.ts's cacheBusterEntry).
|
|
48
54
|
const readable = stream_1.Readable.from(buffer);
|
|
49
|
-
await (0, promises_1.pipeline)(readable, (0, zlib_1.createGunzip)(), (0, tar_1.extract)({
|
|
55
|
+
await (0, promises_1.pipeline)(readable, (0, zlib_1.createGunzip)(), (0, tar_1.extract)({
|
|
56
|
+
cwd: destination,
|
|
57
|
+
strip: 1,
|
|
58
|
+
// `filter` receives the pre-strip path (verified against this
|
|
59
|
+
// version of the `tar` package).
|
|
60
|
+
filter: (entryPath) => {
|
|
61
|
+
const top = entryPath.split('/')[0];
|
|
62
|
+
return top === 'tenantcode' && !/\/sa-nocache-[0-9a-f-]+$/.test(entryPath);
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
50
65
|
console.log(chalk_1.default.green(`Project "${projectName}" pulled successfully!`));
|
|
51
66
|
}
|
|
52
67
|
catch (error) {
|
|
@@ -69,7 +69,7 @@ async function rolePushWithConfig(dir, options, config) {
|
|
|
69
69
|
readResult = await (0, mcp_1.callCrewsTool)(config, 'roles', { action: 'read', name });
|
|
70
70
|
}
|
|
71
71
|
catch (e) {
|
|
72
|
-
process.stderr.write(chalk_1.default.red(`error:
|
|
72
|
+
process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
|
|
73
73
|
process.exit(1);
|
|
74
74
|
}
|
|
75
75
|
if (!readResult.ok) {
|
|
@@ -110,7 +110,7 @@ async function rolePushWithConfig(dir, options, config) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
catch (e) {
|
|
113
|
-
process.stderr.write(chalk_1.default.red(`error:
|
|
113
|
+
process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
|
|
114
114
|
process.exit(1);
|
|
115
115
|
}
|
|
116
116
|
// On name collision, switch to the edit path.
|
|
@@ -126,7 +126,7 @@ async function rolePushWithConfig(dir, options, config) {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
catch (e) {
|
|
129
|
-
process.stderr.write(chalk_1.default.red(`error:
|
|
129
|
+
process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
132
132
|
if (!result.ok) {
|
|
@@ -10,6 +10,7 @@ exports.detailedOutcomeTag = detailedOutcomeTag;
|
|
|
10
10
|
const axios_1 = __importDefault(require("axios"));
|
|
11
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
12
|
const api_1 = require("../utils/api");
|
|
13
|
+
const table_1 = require("../utils/table");
|
|
13
14
|
/**
|
|
14
15
|
* Build the params object for GET /api/v1/runs.
|
|
15
16
|
* Extracted as a pure function so it can be unit-tested without mocking axios.
|
|
@@ -124,19 +125,30 @@ function displaySummaryTable(runsList, projectName) {
|
|
|
124
125
|
const header = projectName ? `Recent runs for "${projectName}":` : 'Recent runs:';
|
|
125
126
|
console.log(chalk_1.default.blue(header));
|
|
126
127
|
console.log('');
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const id = String(run.id || '?').padEnd(8);
|
|
131
|
-
const workflow = truncate(run.workflow_name || '?', 24).padEnd(25);
|
|
132
|
-
const { label: status, attention } = summaryStatusLabel(run);
|
|
133
|
-
const statusColor = attention ? chalk_1.default.yellow : getStatusColor(status);
|
|
128
|
+
const headers = ['ID', 'WORKFLOW', 'STATUS', 'TRIGGERED', 'TRIGGERED BY'];
|
|
129
|
+
const rows = runsList.map((run) => {
|
|
130
|
+
const { label: status } = summaryStatusLabel(run);
|
|
134
131
|
const triggeredAt = run.triggered_at ? new Date(run.triggered_at).toLocaleString() : '-';
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
return [
|
|
133
|
+
String(run.id || '?'),
|
|
134
|
+
(0, table_1.sanitizeCell)((0, table_1.truncateCell)(run.workflow_name || '?', 24)),
|
|
135
|
+
status,
|
|
136
|
+
triggeredAt,
|
|
137
|
+
(0, table_1.sanitizeCell)(run.triggered_by || '-'),
|
|
138
|
+
];
|
|
139
|
+
});
|
|
140
|
+
const widths = (0, table_1.computeColumnWidths)(headers, rows, { minWidths: [8, 25, 12, 22, 0] });
|
|
141
|
+
console.log(chalk_1.default.gray(headers.map((h, i) => h.padEnd(widths[i])).join('')));
|
|
142
|
+
console.log(chalk_1.default.gray('-'.repeat(widths.reduce((a, b) => a + b, 0))));
|
|
143
|
+
for (let i = 0; i < runsList.length; i++) {
|
|
144
|
+
const run = runsList[i];
|
|
145
|
+
const [id, workflow, status, triggeredAt, triggeredBy] = rows[i];
|
|
146
|
+
const { attention } = summaryStatusLabel(run);
|
|
147
|
+
const statusColor = attention ? chalk_1.default.yellow : getStatusColor(status);
|
|
148
|
+
console.log(chalk_1.default.gray(id.padEnd(widths[0])) +
|
|
149
|
+
workflow.padEnd(widths[1]) +
|
|
150
|
+
statusColor(status.padEnd(widths[2])) +
|
|
151
|
+
chalk_1.default.gray(triggeredAt.padEnd(widths[3])) +
|
|
140
152
|
chalk_1.default.gray(triggeredBy));
|
|
141
153
|
}
|
|
142
154
|
console.log('');
|
|
@@ -72,7 +72,13 @@ async function run(projectName, workflowName, options) {
|
|
|
72
72
|
console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
|
|
73
73
|
}
|
|
74
74
|
else if (error.response.status === 404) {
|
|
75
|
-
|
|
75
|
+
const envsList = await (0, api_1.describeProjectEnvironments)(config, projectName);
|
|
76
|
+
if (envsList) {
|
|
77
|
+
console.error(chalk_1.default.red(`Project "${projectName}" has no ${environment} environment (exists in: ${envsList}). Pass -e <env> to target a different environment.`));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.error(chalk_1.default.red('Project or workflow not found.'));
|
|
81
|
+
}
|
|
76
82
|
}
|
|
77
83
|
else if (error.response.status === 422) {
|
|
78
84
|
console.error(chalk_1.default.red('Validation error:'), error.response.data.message);
|
|
@@ -12,6 +12,11 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
12
12
|
const chalk_1 = __importDefault(require("chalk"));
|
|
13
13
|
const api_1 = require("../utils/api");
|
|
14
14
|
async function runView(runId, options) {
|
|
15
|
+
if (!/^\d+$/.test(runId)) {
|
|
16
|
+
console.error(chalk_1.default.red(`Invalid run id: "${runId}" (run ids are numeric — see \`solidactions run list\`).`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
15
20
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
16
21
|
try {
|
|
17
22
|
// Fetch run data (includes session timing)
|
|
@@ -42,7 +42,7 @@ async function skillPullWithConfig(name, dest, options, config) {
|
|
|
42
42
|
result = await (0, mcp_1.callCrewsTool)(config, 'skills', { action: 'read', identifier: name });
|
|
43
43
|
}
|
|
44
44
|
catch (e) {
|
|
45
|
-
process.stderr.write(chalk_1.default.red(`error:
|
|
45
|
+
process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
if (!result.ok) {
|
|
@@ -193,7 +193,7 @@ async function pushParsedSkill(payload, options, config) {
|
|
|
193
193
|
readResult = await (0, mcp_1.callCrewsTool)(config, tool, readArgs);
|
|
194
194
|
}
|
|
195
195
|
catch (e) {
|
|
196
|
-
throw new Error(
|
|
196
|
+
throw new Error(`${e.message}`);
|
|
197
197
|
}
|
|
198
198
|
if (!readResult.ok) {
|
|
199
199
|
const code = readResult.data?.code;
|
|
@@ -215,7 +215,7 @@ async function pushParsedSkill(payload, options, config) {
|
|
|
215
215
|
result = await (0, mcp_1.callCrewsTool)(config, tool, createArgs);
|
|
216
216
|
}
|
|
217
217
|
catch (e) {
|
|
218
|
-
throw new Error(
|
|
218
|
+
throw new Error(`${e.message}`);
|
|
219
219
|
}
|
|
220
220
|
// On name collision, switch to the edit path. properties → properties_patch.
|
|
221
221
|
if (!result.ok && result.data?.code === 'name_collision') {
|
|
@@ -226,7 +226,7 @@ async function pushParsedSkill(payload, options, config) {
|
|
|
226
226
|
result = await (0, mcp_1.callCrewsTool)(config, tool, editArgs);
|
|
227
227
|
}
|
|
228
228
|
catch (e) {
|
|
229
|
-
throw new Error(
|
|
229
|
+
throw new Error(`${e.message}`);
|
|
230
230
|
}
|
|
231
231
|
if (!result.ok) {
|
|
232
232
|
const errData = result.data;
|
package/dist/index.js
CHANGED
|
@@ -71,17 +71,23 @@ program
|
|
|
71
71
|
.name('solidactions')
|
|
72
72
|
.description('SolidActions CLI - Deploy and manage workflow automation')
|
|
73
73
|
.version(pkg.version);
|
|
74
|
-
|
|
74
|
+
// Long form is --workspace-override (NOT --workspace) — `login` already owns
|
|
75
|
+
// `--workspace` for its own purpose (set the workspace at login time), and
|
|
76
|
+
// commander silently drops a subcommand option whose long name collides with
|
|
77
|
+
// a parent option of the same name, so `login --workspace <x>` was being
|
|
78
|
+
// swallowed by this global flag instead of reaching login()'s own option.
|
|
79
|
+
// The short form `-w` is unaffected and remains the primary way to use this.
|
|
80
|
+
program.option('-w, --workspace-override <id-or-slug-or-name>', 'Override active workspace for this command (short form: -w)');
|
|
75
81
|
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
76
82
|
const opts = thisCommand.opts();
|
|
77
|
-
const wsOverride = opts.
|
|
83
|
+
const wsOverride = opts.workspaceOverride;
|
|
78
84
|
if (wsOverride) {
|
|
79
85
|
// workspace set is a write — -w is for read-paths only.
|
|
80
86
|
const fullName = actionCommand.name();
|
|
81
87
|
const parentName = actionCommand.parent?.name?.();
|
|
82
88
|
const isWorkspaceSet = fullName === 'set' && parentName === 'workspace';
|
|
83
89
|
if (isWorkspaceSet) {
|
|
84
|
-
console.error(chalk_1.default.yellow('warn:') + ' -w/--workspace is ignored on `workspace set`; the positional argument is the new workspace.');
|
|
90
|
+
console.error(chalk_1.default.yellow('warn:') + ' -w/--workspace-override is ignored on `workspace set`; the positional argument is the new workspace.');
|
|
85
91
|
return;
|
|
86
92
|
}
|
|
87
93
|
(0, config_1.setCliWorkspaceOverride)(wsOverride);
|
|
@@ -96,7 +102,7 @@ program
|
|
|
96
102
|
.argument('<api-key>', 'Your SolidActions API key')
|
|
97
103
|
.option('--dev', 'Use local development server (http://localhost:8000)')
|
|
98
104
|
.option('--host <url>', 'Custom API host URL')
|
|
99
|
-
.option('--workspace <name-or-id>', 'Set workspace by name, slug, or ID (skips interactive prompt)')
|
|
105
|
+
.option('--workspace <name-or-id>', 'Set workspace by name, slug, or ID (skips interactive prompt). Non-interactive logins without this flag leave the workspace unset.')
|
|
100
106
|
.option('--local', 'Save config to ./.solidactions/config.json in the current folder')
|
|
101
107
|
.option('--global', 'Save config to ~/.solidactions/config.json (default if prompted)')
|
|
102
108
|
.option('--gitignore', 'With --local, add .solidactions/ to .gitignore without prompting')
|
|
@@ -180,15 +186,18 @@ project
|
|
|
180
186
|
.command('logs')
|
|
181
187
|
.description('View build/deployment logs for a project')
|
|
182
188
|
.argument('<project>', 'Project name (or family name with -e)')
|
|
183
|
-
.option('-e, --
|
|
189
|
+
.option('-e, --env <environment>', 'Environment to resolve (production/staging/dev)')
|
|
190
|
+
.addOption(new commander_1.Option('--environment <environment>', 'Alias of --env').hideHelp())
|
|
184
191
|
.action((projectName, options) => {
|
|
185
|
-
|
|
192
|
+
const environment = options.env ?? options.environment;
|
|
193
|
+
(0, project_logs_1.logsBuild)(projectName, environment);
|
|
186
194
|
});
|
|
187
195
|
project
|
|
188
196
|
.command('list')
|
|
189
197
|
.description('List all projects')
|
|
190
|
-
.
|
|
191
|
-
|
|
198
|
+
.option('--json', 'Output as JSON')
|
|
199
|
+
.action((options) => {
|
|
200
|
+
(0, project_list_1.projectList)(options);
|
|
192
201
|
});
|
|
193
202
|
// =============================================================================
|
|
194
203
|
// run <subcommand>
|
|
@@ -217,8 +226,10 @@ runCmd
|
|
|
217
226
|
.option('--detailed', 'Include timeline, steps, and logs per run (default limit: 5)')
|
|
218
227
|
.option('--has-errors', 'Show only runs with errors (step errors, retries, or degraded results)')
|
|
219
228
|
.option('--json', 'Output as JSON')
|
|
220
|
-
.option('-e, --
|
|
229
|
+
.option('-e, --env <environment>', 'Environment to filter by (production/staging/dev)')
|
|
230
|
+
.addOption(new commander_1.Option('--environment <environment>', 'Alias of --env').hideHelp())
|
|
221
231
|
.action((projectName, options) => {
|
|
232
|
+
options.environment = options.env ?? options.environment;
|
|
222
233
|
(0, run_list_1.runs)(projectName, options);
|
|
223
234
|
});
|
|
224
235
|
runCmd
|
|
@@ -235,10 +246,10 @@ runCmd
|
|
|
235
246
|
// =============================================================================
|
|
236
247
|
// env <subcommand>
|
|
237
248
|
// =============================================================================
|
|
238
|
-
const env = program.command('env').description('Manage
|
|
249
|
+
const env = program.command('env').description('Manage variables');
|
|
239
250
|
env
|
|
240
251
|
.command('set')
|
|
241
|
-
.description('Set
|
|
252
|
+
.description('Set a variable (create or update, global or project)')
|
|
242
253
|
.argument('<key-or-project>', 'Variable key (global) or project name')
|
|
243
254
|
.argument('<value-or-key>', 'Variable value (global) or variable key (project)')
|
|
244
255
|
.argument('[value]', 'Variable value (when first arg is project)')
|
|
@@ -256,17 +267,19 @@ env
|
|
|
256
267
|
});
|
|
257
268
|
env
|
|
258
269
|
.command('list')
|
|
259
|
-
.description('List
|
|
270
|
+
.description('List variables')
|
|
260
271
|
.argument('[project]', 'Project name (omit for global variables)')
|
|
261
272
|
.option('-e, --env <environment>', 'Filter by environment (production/staging/dev)')
|
|
273
|
+
.option('--json', 'Output as JSON')
|
|
262
274
|
.action((projectName, options) => {
|
|
263
275
|
(0, env_list_1.envList)(projectName, options);
|
|
264
276
|
});
|
|
265
277
|
env
|
|
266
278
|
.command('delete')
|
|
267
|
-
.description('Delete
|
|
279
|
+
.description('Delete a variable')
|
|
268
280
|
.argument('<key-or-project>', 'Variable key (global) or project name')
|
|
269
281
|
.argument('[key]', 'Variable key (if first arg is project)')
|
|
282
|
+
.option('-e, --env <environment>', 'Environment to delete from', 'dev')
|
|
270
283
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
271
284
|
.action((keyOrProject, key, options) => {
|
|
272
285
|
(0, env_delete_1.envDelete)(keyOrProject, key, options);
|
|
@@ -283,7 +296,7 @@ env
|
|
|
283
296
|
});
|
|
284
297
|
env
|
|
285
298
|
.command('pull')
|
|
286
|
-
.description('Pull resolved
|
|
299
|
+
.description('Pull resolved variables to a local file')
|
|
287
300
|
.argument('<project>', 'Project name')
|
|
288
301
|
.option('-e, --env <environment>', 'Environment (production/staging/dev)', 'dev')
|
|
289
302
|
.option('-o, --output <file>', 'Output file path (defaults to .env or .env.{environment})')
|
|
@@ -294,7 +307,7 @@ env
|
|
|
294
307
|
});
|
|
295
308
|
env
|
|
296
309
|
.command('push')
|
|
297
|
-
.description('Push
|
|
310
|
+
.description('Push variables from .env file to a project')
|
|
298
311
|
.argument('<project>', 'Project name')
|
|
299
312
|
.argument('[path]', 'Source directory with solidactions.yaml and .env file', '.')
|
|
300
313
|
.option('-e, --env <environment>', 'Target environment (production/staging/dev)', 'dev')
|
|
@@ -313,7 +326,7 @@ schedule
|
|
|
313
326
|
.description('Set a cron schedule for a workflow')
|
|
314
327
|
.argument('<project>', 'Project name')
|
|
315
328
|
.argument('<cron>', 'Cron expression (e.g., "0 9 * * *" for daily at 9am)')
|
|
316
|
-
.option('
|
|
329
|
+
.option('--workflow <name>', 'Workflow name (if project has multiple)')
|
|
317
330
|
.option('-i, --input <json>', 'JSON input to pass to scheduled runs')
|
|
318
331
|
.option('-z, --timezone <iana>', 'IANA timezone the cron is evaluated in (e.g. America/Chicago); defaults to UTC')
|
|
319
332
|
.option('-y, --yes', 'Skip confirmation if schedule already exists')
|
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 = '';
|