@solidactions/cli 1.20.0 → 1.22.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 +78 -11
- package/dist/commands/dev-shim.mjs +194 -0
- package/dist/commands/dev.js +165 -7
- package/dist/commands/env-map.js +5 -0
- package/dist/commands/env-push.js +9 -0
- package/dist/commands/env-set.js +33 -0
- package/dist/commands/init.js +7 -4
- package/dist/commands/login.js +22 -8
- package/dist/commands/project-logs.js +26 -3
- package/dist/commands/run-list.js +41 -19
- package/dist/commands/run-view.js +54 -4
- package/dist/commands/schedule-list.js +4 -2
- package/dist/commands/schedule-set.js +42 -10
- package/dist/commands/webhook-secret.js +74 -0
- package/dist/index.js +23 -4
- package/dist/utils/env.js +19 -0
- package/dist/utils/skills.js +19 -0
- package/package.json +2 -2
package/dist/commands/env-set.js
CHANGED
|
@@ -3,11 +3,25 @@ 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.GLOBAL_ENV_SCOPE_NOTE = void 0;
|
|
7
|
+
exports.isNonTty = isNonTty;
|
|
6
8
|
exports.envSet = envSet;
|
|
7
9
|
const axios_1 = __importDefault(require("axios"));
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
11
|
const prompts_1 = __importDefault(require("prompts"));
|
|
10
12
|
const api_1 = require("../utils/api");
|
|
13
|
+
const env_1 = require("../utils/env");
|
|
14
|
+
/** Returns true when stdin is not an interactive terminal (CI, pipes, scripts). */
|
|
15
|
+
function isNonTty() {
|
|
16
|
+
return !process.stdin.isTTY;
|
|
17
|
+
}
|
|
18
|
+
/** Printed before a 2-arg (global-scope) write — Jordan's runs 3-7 footgun. */
|
|
19
|
+
exports.GLOBAL_ENV_SCOPE_NOTE = [
|
|
20
|
+
'Note: no project specified — creating a GLOBAL variable.',
|
|
21
|
+
" Global variables are NOT visible to a project's plain `env:` YAML declarations",
|
|
22
|
+
' unless you map them (`solidactions env map …`).',
|
|
23
|
+
' For a project variable, use: solidactions env set <project> KEY value',
|
|
24
|
+
].join('\n');
|
|
11
25
|
async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
12
26
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
13
27
|
// Detect mode based on arguments
|
|
@@ -17,6 +31,10 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
|
17
31
|
const projectName = keyOrProject;
|
|
18
32
|
const key = valueOrKey;
|
|
19
33
|
const value = valueIfProject;
|
|
34
|
+
if ((0, env_1.isReservedEnvName)(key)) {
|
|
35
|
+
console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(key)));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
20
38
|
const environment = options.env || 'dev';
|
|
21
39
|
// Build project slug
|
|
22
40
|
const projectSlug = environment === 'production'
|
|
@@ -31,6 +49,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
|
31
49
|
const mappings = mappingsResponse.data || [];
|
|
32
50
|
const existing = mappings.find((m) => m.env_name === key);
|
|
33
51
|
if (existing && existing.has_value) {
|
|
52
|
+
if (isNonTty()) {
|
|
53
|
+
console.error(chalk_1.default.red(`Variable "${key}" already has a value in "${projectName}" (${environment}). ` +
|
|
54
|
+
`Pass -y / --yes to overwrite without confirmation.`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
34
57
|
console.log(chalk_1.default.yellow(`Variable "${key}" already has a value in "${projectName}" (${environment}).`));
|
|
35
58
|
const confirm = await (0, prompts_1.default)({
|
|
36
59
|
type: 'confirm',
|
|
@@ -83,6 +106,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
|
83
106
|
// Global mode: solidactions env set <key> <value>
|
|
84
107
|
const key = keyOrProject;
|
|
85
108
|
const value = valueOrKey;
|
|
109
|
+
if ((0, env_1.isReservedEnvName)(key)) {
|
|
110
|
+
console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(key)));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.log(chalk_1.default.yellow(exports.GLOBAL_ENV_SCOPE_NOTE));
|
|
86
114
|
const isSecret = options.secret || false;
|
|
87
115
|
// Build the request body with per-environment values
|
|
88
116
|
const body = {
|
|
@@ -119,6 +147,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
|
119
147
|
let action;
|
|
120
148
|
if (existing) {
|
|
121
149
|
if (!options.yes) {
|
|
150
|
+
if (isNonTty()) {
|
|
151
|
+
console.error(chalk_1.default.red(`Global variable "${key}" already exists. ` +
|
|
152
|
+
`Pass -y / --yes to overwrite without confirmation.`));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
122
155
|
console.log(chalk_1.default.yellow(`Global variable "${key}" already exists.`));
|
|
123
156
|
const confirm = await (0, prompts_1.default)({
|
|
124
157
|
type: 'confirm',
|
package/dist/commands/init.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.TEMPLATE_FILES = void 0;
|
|
6
7
|
exports.init = init;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -14,8 +15,9 @@ const EXAMPLES_OWNER = 'SolidActions';
|
|
|
14
15
|
const EXAMPLES_REPO = 'solidactions-examples';
|
|
15
16
|
const TEMPLATE_PREFIX = 'templates/minimal';
|
|
16
17
|
// Tuples of [remote-path-suffix-under-template-prefix, local-path-relative-to-target].
|
|
17
|
-
|
|
18
|
+
exports.TEMPLATE_FILES = [
|
|
18
19
|
['package.json', 'package.json'],
|
|
20
|
+
['package-lock.json', 'package-lock.json'],
|
|
19
21
|
['tsconfig.json', 'tsconfig.json'],
|
|
20
22
|
['solidactions.yaml', 'solidactions.yaml'],
|
|
21
23
|
['.env.example', '.env.example'],
|
|
@@ -48,7 +50,7 @@ async function init(directory, options = {}) {
|
|
|
48
50
|
process.exit(1);
|
|
49
51
|
}
|
|
50
52
|
console.log(chalk_1.default.blue(`Scaffolding "${projectName}" in ${targetDir}...`));
|
|
51
|
-
for (const [remoteSuffix, localSuffix] of TEMPLATE_FILES) {
|
|
53
|
+
for (const [remoteSuffix, localSuffix] of exports.TEMPLATE_FILES) {
|
|
52
54
|
const remotePath = `${TEMPLATE_PREFIX}/${remoteSuffix}`;
|
|
53
55
|
let content = await (0, github_1.fetchRawFile)(EXAMPLES_OWNER, EXAMPLES_REPO, remotePath);
|
|
54
56
|
content = content.replace(/__PROJECT_NAME__/g, projectName);
|
|
@@ -69,9 +71,10 @@ async function init(directory, options = {}) {
|
|
|
69
71
|
if (directory) {
|
|
70
72
|
console.log(chalk_1.default.gray(` cd ${directory}`));
|
|
71
73
|
}
|
|
72
|
-
console.log(chalk_1.default.gray(` # Fill in WEBHOOK_SECRET and any other env vars in solidactions.yaml:`));
|
|
73
|
-
console.log(chalk_1.default.gray(` solidactions env set ${projectName} WEBHOOK_SECRET <your-secret> -e production`));
|
|
74
74
|
console.log(chalk_1.default.gray(` solidactions project deploy ${projectName} -e production`));
|
|
75
|
+
console.log(chalk_1.default.gray(` # If your workflow uses a webhook, retrieve its auto-generated secret`));
|
|
76
|
+
console.log(chalk_1.default.gray(` # and set that value in your sender (e.g. Telegram secret_token):`));
|
|
77
|
+
console.log(chalk_1.default.gray(` solidactions webhook secret ${projectName} -e production`));
|
|
75
78
|
}
|
|
76
79
|
catch (err) {
|
|
77
80
|
if (err.message?.includes('rate limit')) {
|
package/dist/commands/login.js
CHANGED
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.getConfig = getConfig;
|
|
7
7
|
exports.saveConfig = saveConfig;
|
|
8
8
|
exports.clearConfig = clearConfig;
|
|
9
|
+
exports.resolveLoginHost = resolveLoginHost;
|
|
10
|
+
exports.loginHostLines = loginHostLines;
|
|
9
11
|
exports.login = login;
|
|
10
12
|
exports.logout = logout;
|
|
11
13
|
exports.whoami = whoami;
|
|
@@ -26,17 +28,27 @@ function saveConfig(config) {
|
|
|
26
28
|
function clearConfig() {
|
|
27
29
|
(0, config_1.removeConfigFile)((0, config_1.getGlobalConfigPath)());
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
let host;
|
|
31
|
+
function resolveLoginHost(options) {
|
|
31
32
|
if (options.host) {
|
|
32
|
-
host
|
|
33
|
+
return { host: options.host, isDefault: false };
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
host
|
|
35
|
+
if (options.dev) {
|
|
36
|
+
return { host: 'http://localhost:8000', isDefault: false };
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
return { host: 'https://app.solidactions.com', isDefault: true };
|
|
39
|
+
}
|
|
40
|
+
function loginHostLines(resolved) {
|
|
41
|
+
if (resolved.isDefault) {
|
|
42
|
+
return [
|
|
43
|
+
`Logging into ${resolved.host} (SolidActions Cloud)`,
|
|
44
|
+
' Self-hosted or local dev? Pass --host <url> (or --dev for http://localhost:8000)',
|
|
45
|
+
];
|
|
39
46
|
}
|
|
47
|
+
return [`Host: ${resolved.host}`];
|
|
48
|
+
}
|
|
49
|
+
async function login(apiKey, options) {
|
|
50
|
+
const resolved = resolveLoginHost(options);
|
|
51
|
+
const host = resolved.host;
|
|
40
52
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
41
53
|
console.error(chalk_1.default.red('Error: API key is required.'));
|
|
42
54
|
console.log(chalk_1.default.gray('Generate an API key at: ') + chalk_1.default.blue(`${host}/settings/api-keys`));
|
|
@@ -46,7 +58,9 @@ async function login(apiKey, options) {
|
|
|
46
58
|
const target = await (0, config_write_target_1.decideWriteTarget)({ local: options.local, global: options.global });
|
|
47
59
|
const targetPath = (0, config_write_target_1.pathForTarget)(target);
|
|
48
60
|
console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
|
|
49
|
-
|
|
61
|
+
for (const line of loginHostLines(resolved)) {
|
|
62
|
+
console.log(resolved.isDefault ? chalk_1.default.yellow(line) : chalk_1.default.gray(line));
|
|
63
|
+
}
|
|
50
64
|
if ((0, config_1.readConfigFile)(targetPath)) {
|
|
51
65
|
console.log(chalk_1.default.yellow(`Existing config at ${targetPath} will be overwritten.`));
|
|
52
66
|
}
|
|
@@ -3,16 +3,35 @@ 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.buildBuildLogUrl = buildBuildLogUrl;
|
|
7
|
+
exports.buildBuildLogRequest = buildBuildLogRequest;
|
|
6
8
|
exports.logsBuild = logsBuild;
|
|
7
9
|
const axios_1 = __importDefault(require("axios"));
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
11
|
const api_1 = require("../utils/api");
|
|
10
|
-
|
|
12
|
+
function buildBuildLogUrl(host, projectName, environment) {
|
|
13
|
+
if (environment) {
|
|
14
|
+
return `${host}/api/v1/projects/resolve/build-log`;
|
|
15
|
+
}
|
|
16
|
+
return `${host}/api/v1/projects/${projectName}/build-log`;
|
|
17
|
+
}
|
|
18
|
+
function buildBuildLogRequest(host, projectName, environment) {
|
|
19
|
+
if (environment) {
|
|
20
|
+
return {
|
|
21
|
+
url: `${host}/api/v1/projects/resolve/build-log`,
|
|
22
|
+
params: { name: projectName, environment },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { url: `${host}/api/v1/projects/${projectName}/build-log` };
|
|
26
|
+
}
|
|
27
|
+
async function logsBuild(projectName, environment) {
|
|
11
28
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
12
29
|
console.log(chalk_1.default.blue(`Fetching build logs for project "${projectName}"...`));
|
|
30
|
+
const { url, params } = buildBuildLogRequest(config.host, projectName, environment);
|
|
13
31
|
try {
|
|
14
|
-
const response = await axios_1.default.get(
|
|
32
|
+
const response = await axios_1.default.get(url, {
|
|
15
33
|
headers: (0, api_1.getApiHeaders)(config),
|
|
34
|
+
params,
|
|
16
35
|
});
|
|
17
36
|
const buildLog = response.data.build_log || response.data;
|
|
18
37
|
if (!buildLog || buildLog.length === 0) {
|
|
@@ -29,7 +48,11 @@ async function logsBuild(projectName) {
|
|
|
29
48
|
console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
|
|
30
49
|
}
|
|
31
50
|
else if (error.response.status === 404) {
|
|
32
|
-
console.error(chalk_1.default.red(`Project "${projectName}" not found.`));
|
|
51
|
+
console.error(chalk_1.default.red(error.response.data?.message ?? `Project "${projectName}" not found.`));
|
|
52
|
+
const envs = error.response.data?.available_environments;
|
|
53
|
+
if (envs && envs.length > 0) {
|
|
54
|
+
console.error(chalk_1.default.yellow(`Available environments: ${envs.join(', ')}. Pass -e <env> to select one.`));
|
|
55
|
+
}
|
|
33
56
|
}
|
|
34
57
|
else {
|
|
35
58
|
console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
|
|
@@ -3,38 +3,54 @@ 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.buildRunListParams = buildRunListParams;
|
|
6
7
|
exports.runs = runs;
|
|
7
8
|
exports.summaryStatusLabel = summaryStatusLabel;
|
|
8
9
|
exports.detailedOutcomeTag = detailedOutcomeTag;
|
|
9
10
|
const axios_1 = __importDefault(require("axios"));
|
|
10
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
12
|
const api_1 = require("../utils/api");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Build the params object for GET /api/v1/runs.
|
|
15
|
+
* Extracted as a pure function so it can be unit-tested without mocking axios.
|
|
16
|
+
*/
|
|
17
|
+
function buildRunListParams(projectName, options = {}) {
|
|
15
18
|
const defaultLimit = options.detailed ? 5 : 20;
|
|
16
19
|
const limit = options.limit || defaultLimit;
|
|
20
|
+
const params = { limit };
|
|
21
|
+
if (projectName)
|
|
22
|
+
params.project = projectName;
|
|
23
|
+
if (options.environment)
|
|
24
|
+
params.environment = options.environment;
|
|
25
|
+
if (options.offset)
|
|
26
|
+
params.offset = options.offset;
|
|
27
|
+
if (options.status)
|
|
28
|
+
params.status = options.status;
|
|
29
|
+
if (options.since)
|
|
30
|
+
params.since = parseSince(options.since);
|
|
31
|
+
if (options.workflow)
|
|
32
|
+
params.workflow = options.workflow;
|
|
33
|
+
if (options.detailed)
|
|
34
|
+
params.detailed = '1';
|
|
35
|
+
if (options.hasErrors)
|
|
36
|
+
params.has_errors = '1';
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
async function runs(projectName, options = {}) {
|
|
40
|
+
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
17
41
|
try {
|
|
18
|
-
const params =
|
|
19
|
-
if (projectName)
|
|
20
|
-
params.project = projectName;
|
|
21
|
-
if (options.offset)
|
|
22
|
-
params.offset = options.offset;
|
|
23
|
-
if (options.status)
|
|
24
|
-
params.status = options.status;
|
|
25
|
-
if (options.since)
|
|
26
|
-
params.since = parseSince(options.since);
|
|
27
|
-
if (options.workflow)
|
|
28
|
-
params.workflow = options.workflow;
|
|
29
|
-
if (options.detailed)
|
|
30
|
-
params.detailed = '1';
|
|
31
|
-
if (options.hasErrors)
|
|
32
|
-
params.has_errors = '1';
|
|
42
|
+
const params = buildRunListParams(projectName, options);
|
|
33
43
|
const response = await axios_1.default.get(`${config.host}/api/v1/runs`, {
|
|
34
44
|
headers: (0, api_1.getApiHeaders)(config),
|
|
35
45
|
params,
|
|
36
46
|
});
|
|
37
47
|
const runsList = response.data.data || response.data;
|
|
48
|
+
// Check server-side error fields FIRST (project_not_found comes back as 200 with data:[])
|
|
49
|
+
const serverError = response.data.error;
|
|
50
|
+
if (serverError === 'project_not_found') {
|
|
51
|
+
console.error(chalk_1.default.red(response.data.message || `Project '${projectName}' not found.`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
38
54
|
if (!runsList || runsList.length === 0) {
|
|
39
55
|
if (options.json) {
|
|
40
56
|
console.log('[]');
|
|
@@ -58,7 +74,13 @@ async function runs(projectName, options = {}) {
|
|
|
58
74
|
}
|
|
59
75
|
catch (error) {
|
|
60
76
|
if (error.response) {
|
|
61
|
-
|
|
77
|
+
// ambiguous_project is 422 — axios throws, so handle here
|
|
78
|
+
if (error.response.status === 422 && error.response.data?.error === 'ambiguous_project') {
|
|
79
|
+
const envs = (error.response.data.environments ?? []).join(', ');
|
|
80
|
+
console.error(chalk_1.default.yellow(`Ambiguous project '${projectName}': found in environments: ${envs}.`));
|
|
81
|
+
console.error(chalk_1.default.yellow(`Pass -e <env> to disambiguate, e.g.: solidactions run list ${projectName} -e dev`));
|
|
82
|
+
}
|
|
83
|
+
else if (error.response.status === 401) {
|
|
62
84
|
console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
|
|
63
85
|
}
|
|
64
86
|
else {
|
|
@@ -4,6 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runView = runView;
|
|
7
|
+
exports.displayStepsTable = displayStepsTable;
|
|
8
|
+
exports.flattenSteps = flattenSteps;
|
|
9
|
+
exports.stepDisplayStatus = stepDisplayStatus;
|
|
10
|
+
exports.errorMessage = errorMessage;
|
|
7
11
|
const axios_1 = __importDefault(require("axios"));
|
|
8
12
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
13
|
const api_1 = require("../utils/api");
|
|
@@ -149,7 +153,7 @@ function displayFullView(runData, timeline, steps, logsData) {
|
|
|
149
153
|
if (runData.error) {
|
|
150
154
|
console.log('');
|
|
151
155
|
console.log(chalk_1.default.bold.red(' Error:'));
|
|
152
|
-
console.log(chalk_1.default.red(` ${runData.error}`));
|
|
156
|
+
console.log(chalk_1.default.red(` ${errorMessage(runData.error)}`));
|
|
153
157
|
}
|
|
154
158
|
// Logs snippet
|
|
155
159
|
const logs = logsData.logs || '';
|
|
@@ -203,11 +207,22 @@ function displayStepsTable(steps, indent = ' ') {
|
|
|
203
207
|
console.log(chalk_1.default.gray(`${indent}${'─'.repeat(72)}`));
|
|
204
208
|
for (const step of steps) {
|
|
205
209
|
const name = (step.name || '?').padEnd(24);
|
|
206
|
-
const status = step
|
|
210
|
+
const status = stepDisplayStatus(step);
|
|
207
211
|
const statusColor = getStatusColor(status);
|
|
208
212
|
const duration = formatDuration(step.durationMs);
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
// A failed step's error is more useful than its (null) output — show it, red.
|
|
214
|
+
const output = step.error
|
|
215
|
+
? chalk_1.default.red(truncate(errorMessage(step.error).replace(/\s+/g, ' ').trim(), 40))
|
|
216
|
+
: chalk_1.default.gray(step.output ? truncate(JSON.stringify(unwrapOutput(step.output)), 40) : '-');
|
|
217
|
+
console.log(`${indent}${name}${statusColor(status.padEnd(12))}${chalk_1.default.gray(duration.padEnd(12))}${output}`);
|
|
218
|
+
}
|
|
219
|
+
// Untruncated first failure below the table: `run view` alone must be
|
|
220
|
+
// enough to diagnose (Wall #5 — failed steps used to render "completed").
|
|
221
|
+
const firstFailed = steps.find((s) => stepDisplayStatus(s).toLowerCase() === 'failed' && s.error);
|
|
222
|
+
if (firstFailed) {
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(chalk_1.default.bold.red(`${indent}Step "${firstFailed.name}" failed:`));
|
|
225
|
+
console.log(chalk_1.default.red(`${indent} ${errorMessage(firstFailed.error)}`));
|
|
211
226
|
}
|
|
212
227
|
}
|
|
213
228
|
// ─── Utility ───────────────────────────────────────────────────────────────
|
|
@@ -221,17 +236,52 @@ function flattenSteps(workers) {
|
|
|
221
236
|
completedAt: step.completed_at || null,
|
|
222
237
|
durationMs: step.duration_ms ?? null,
|
|
223
238
|
output: step.output ?? null,
|
|
239
|
+
status: step.status ?? null,
|
|
240
|
+
error: step.error ?? null,
|
|
224
241
|
});
|
|
225
242
|
}
|
|
226
243
|
}
|
|
227
244
|
return steps;
|
|
228
245
|
}
|
|
246
|
+
/** Server-derived status wins; timestamp heuristic only for pre-status API responses. */
|
|
247
|
+
function stepDisplayStatus(step) {
|
|
248
|
+
return step.status ?? (step.completedAt ? 'completed' : step.startedAt ? 'running' : 'pending');
|
|
249
|
+
}
|
|
229
250
|
function unwrapOutput(output) {
|
|
230
251
|
if (output && output.__solidactions_serializer === 'superjson' && output.json) {
|
|
231
252
|
return output.json;
|
|
232
253
|
}
|
|
233
254
|
return output;
|
|
234
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Normalize a step/run `error` field to a readable string. Errors arrive in
|
|
258
|
+
* one of three shapes: a plain string (legacy servers), a superjson-wrapped
|
|
259
|
+
* `{ json: { name, message, stack }, __solidactions_serializer: 'superjson' }`
|
|
260
|
+
* object (steps endpoint), or that same shape JSON.stringified into a string
|
|
261
|
+
* (run-level `error` field) — confirmed live via GET /api/v1/runs/{id} and
|
|
262
|
+
* .../steps. Without this, a thrown Error renders as "[object Object]".
|
|
263
|
+
*/
|
|
264
|
+
function errorMessage(error) {
|
|
265
|
+
if (error === null || error === undefined)
|
|
266
|
+
return '';
|
|
267
|
+
let value = error;
|
|
268
|
+
if (typeof value === 'string') {
|
|
269
|
+
try {
|
|
270
|
+
value = JSON.parse(value);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (value && typeof value === 'object' && value.__solidactions_serializer === 'superjson' && value.json) {
|
|
277
|
+
const inner = value.json;
|
|
278
|
+
if (inner && typeof inner === 'object' && (inner.name || inner.message)) {
|
|
279
|
+
return inner.name && inner.message ? `${inner.name}: ${inner.message}` : String(inner.name || inner.message);
|
|
280
|
+
}
|
|
281
|
+
return JSON.stringify(inner);
|
|
282
|
+
}
|
|
283
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
284
|
+
}
|
|
235
285
|
function formatDuration(ms) {
|
|
236
286
|
if (ms === null || ms === undefined)
|
|
237
287
|
return '-';
|
|
@@ -20,18 +20,20 @@ async function scheduleList(projectName) {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
console.log('');
|
|
23
|
-
console.log(chalk_1.default.gray('ID'.padEnd(8) + 'WORKFLOW'.padEnd(25) + 'CRON'.padEnd(18) + 'ENABLED'.padEnd(10) + 'NEXT RUN'));
|
|
24
|
-
console.log(chalk_1.default.gray('-'.repeat(
|
|
23
|
+
console.log(chalk_1.default.gray('ID'.padEnd(8) + 'WORKFLOW'.padEnd(25) + 'CRON'.padEnd(18) + 'TIMEZONE'.padEnd(20) + 'ENABLED'.padEnd(10) + 'NEXT RUN'));
|
|
24
|
+
console.log(chalk_1.default.gray('-'.repeat(105)));
|
|
25
25
|
for (const schedule of schedules) {
|
|
26
26
|
const id = schedule.id?.toString() || '?';
|
|
27
27
|
const workflow = schedule.workflow_name || schedule.workflow_slug || '?';
|
|
28
28
|
const cron = schedule.cron_expression || '?';
|
|
29
|
+
const timezone = schedule.timezone || 'UTC';
|
|
29
30
|
const enabled = schedule.enabled;
|
|
30
31
|
const nextRun = schedule.next_run_at ? formatRelativeTime(schedule.next_run_at) : '-';
|
|
31
32
|
const enabledColor = enabled ? chalk_1.default.green : chalk_1.default.red;
|
|
32
33
|
console.log(chalk_1.default.gray(id.padEnd(8)) +
|
|
33
34
|
workflow.padEnd(25) +
|
|
34
35
|
chalk_1.default.cyan(cron.padEnd(18)) +
|
|
36
|
+
chalk_1.default.gray(timezone.padEnd(20)) +
|
|
35
37
|
enabledColor((enabled ? 'yes' : 'no').padEnd(10)) +
|
|
36
38
|
chalk_1.default.gray(nextRun));
|
|
37
39
|
}
|
|
@@ -3,11 +3,38 @@ 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.buildSchedulePayload = buildSchedulePayload;
|
|
7
|
+
exports.timezoneMismatch = timezoneMismatch;
|
|
8
|
+
exports.timezoneMismatchRemedy = timezoneMismatchRemedy;
|
|
6
9
|
exports.scheduleSet = scheduleSet;
|
|
7
10
|
const axios_1 = __importDefault(require("axios"));
|
|
8
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
12
|
const prompts_1 = __importDefault(require("prompts"));
|
|
10
13
|
const api_1 = require("../utils/api");
|
|
14
|
+
function buildSchedulePayload(cron, options, inputData) {
|
|
15
|
+
const payload = { cron };
|
|
16
|
+
if (options.workflow) {
|
|
17
|
+
payload.workflow = options.workflow;
|
|
18
|
+
}
|
|
19
|
+
if (inputData) {
|
|
20
|
+
payload.input = inputData;
|
|
21
|
+
}
|
|
22
|
+
if (options.timezone) {
|
|
23
|
+
payload.timezone = options.timezone;
|
|
24
|
+
}
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
/** True when the user asked for a timezone the server did not apply (pre-item-5-App server). */
|
|
28
|
+
function timezoneMismatch(requested, returned) {
|
|
29
|
+
return requested !== undefined && returned !== requested;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Remediation text for a timezoneMismatch. `schedule delete` takes a numeric
|
|
33
|
+
* <schedule-id>, not a workflow name — point at `schedule list` first to find it.
|
|
34
|
+
*/
|
|
35
|
+
function timezoneMismatchRemedy(projectName) {
|
|
36
|
+
return `Your server may not support schedule timezones yet; update the server, or run 'solidactions schedule list ${projectName}' to find the schedule ID and remove it with: solidactions schedule delete ${projectName} <schedule-id>`;
|
|
37
|
+
}
|
|
11
38
|
async function scheduleSet(projectName, cron, options) {
|
|
12
39
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
13
40
|
// Parse input JSON if provided
|
|
@@ -66,20 +93,25 @@ async function scheduleSet(projectName, cron, options) {
|
|
|
66
93
|
}
|
|
67
94
|
console.log(chalk_1.default.blue(`Setting schedule for project "${projectName}"...`));
|
|
68
95
|
try {
|
|
69
|
-
const payload =
|
|
70
|
-
|
|
71
|
-
};
|
|
72
|
-
if (options.workflow) {
|
|
73
|
-
payload.workflow = options.workflow;
|
|
74
|
-
}
|
|
75
|
-
if (inputData) {
|
|
76
|
-
payload.input = inputData;
|
|
77
|
-
}
|
|
78
|
-
await axios_1.default.post(`${config.host}/api/v1/projects/${projectName}/schedules`, payload, {
|
|
96
|
+
const payload = buildSchedulePayload(cron, options, inputData);
|
|
97
|
+
const response = await axios_1.default.post(`${config.host}/api/v1/projects/${projectName}/schedules`, payload, {
|
|
79
98
|
headers: (0, api_1.getApiHeaders)(config, 'application/json'),
|
|
80
99
|
});
|
|
100
|
+
// Verify, don't trust: a server predating timezone support silently
|
|
101
|
+
// strips the field, but the schedule is already persisted by this point —
|
|
102
|
+
// it's live and running in the wrong timezone. Report the persisted fact
|
|
103
|
+
// and the remedy, not a hypothetical.
|
|
104
|
+
const returnedTz = response.data?.schedule?.timezone;
|
|
105
|
+
if (timezoneMismatch(options.timezone, returnedTz)) {
|
|
106
|
+
console.error(chalk_1.default.red(`A schedule was created but is running in ${returnedTz ?? 'UTC'} — not ${options.timezone} as requested.`));
|
|
107
|
+
console.error(chalk_1.default.red(timezoneMismatchRemedy(projectName)));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
81
110
|
console.log(chalk_1.default.green(`Schedule set successfully!`));
|
|
82
111
|
console.log(chalk_1.default.gray(` Cron: ${cron}`));
|
|
112
|
+
if (options.timezone) {
|
|
113
|
+
console.log(chalk_1.default.gray(` Timezone: ${options.timezone}`));
|
|
114
|
+
}
|
|
83
115
|
if (options.workflow) {
|
|
84
116
|
console.log(chalk_1.default.gray(` Workflow: ${options.workflow}`));
|
|
85
117
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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.formatSecretOutput = formatSecretOutput;
|
|
7
|
+
exports.webhookSecret = webhookSecret;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const api_1 = require("../utils/api");
|
|
11
|
+
function formatSecretOutput(rows, workflowFilter) {
|
|
12
|
+
if (workflowFilter) {
|
|
13
|
+
const match = rows.find((r) => r.workflow_name === workflowFilter);
|
|
14
|
+
if (!match)
|
|
15
|
+
return `No webhook named "${workflowFilter}" found.`;
|
|
16
|
+
return match.webhook_secret ?? '(secret not returned — check your access level)';
|
|
17
|
+
}
|
|
18
|
+
if (rows.length === 1) {
|
|
19
|
+
return rows[0].webhook_secret ?? '(secret not returned — check your access level)';
|
|
20
|
+
}
|
|
21
|
+
const lines = rows.map((r) => `${(r.workflow_name ?? '?').padEnd(32)}${r.webhook_secret ?? '(none)'}`);
|
|
22
|
+
return ['WORKFLOW'.padEnd(32) + 'SECRET', '-'.repeat(80), ...lines].join('\n');
|
|
23
|
+
}
|
|
24
|
+
async function webhookSecret(projectName, options = {}) {
|
|
25
|
+
const format = options.format ?? 'text';
|
|
26
|
+
if (format !== 'text' && format !== 'json') {
|
|
27
|
+
console.error(chalk_1.default.red(`Invalid --format: ${format}. Expected 'text' or 'json'.`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
31
|
+
const environment = options.env || 'dev';
|
|
32
|
+
const projectSlug = environment === 'production' ? projectName : `${projectName}-${environment}`;
|
|
33
|
+
if (format === 'text') {
|
|
34
|
+
process.stderr.write(chalk_1.default.dim(`(environment: ${environment} — pass -e <env> to change)\n`));
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/webhooks`, {
|
|
38
|
+
headers: (0, api_1.getApiHeaders)(config),
|
|
39
|
+
params: { show_secrets: 'true' },
|
|
40
|
+
});
|
|
41
|
+
const rows = response.data.data ?? [];
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
console.error(chalk_1.default.yellow(`No webhook workflows found for project "${projectName}"${environment !== 'production' ? ` (${environment})` : ''}.`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (format === 'json') {
|
|
47
|
+
const out = options.workflow
|
|
48
|
+
? rows
|
|
49
|
+
.filter((r) => r.workflow_name === options.workflow)
|
|
50
|
+
.map((r) => ({ workflow: r.workflow_name, secret: r.webhook_secret }))
|
|
51
|
+
: rows.map((r) => ({ workflow: r.workflow_name, secret: r.webhook_secret }));
|
|
52
|
+
console.log(JSON.stringify(out, null, 2));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(formatSecretOutput(rows, options.workflow));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.response) {
|
|
59
|
+
if (error.response.status === 401) {
|
|
60
|
+
console.error(chalk_1.default.red('Authentication failed. Run "solidactions login <api-key>" to re-configure.'));
|
|
61
|
+
}
|
|
62
|
+
else if (error.response.status === 404) {
|
|
63
|
+
console.error(chalk_1.default.red(`Project "${projectName}" not found.`));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error(chalk_1.default.red(`Failed: ${error.response.status}`), error.response.data);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.error(chalk_1.default.red('Connection failed:'), error.message);
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ const schedule_set_1 = require("./commands/schedule-set");
|
|
|
26
26
|
const schedule_list_1 = require("./commands/schedule-list");
|
|
27
27
|
const schedule_delete_1 = require("./commands/schedule-delete");
|
|
28
28
|
const webhook_list_1 = require("./commands/webhook-list");
|
|
29
|
+
const webhook_secret_1 = require("./commands/webhook-secret");
|
|
29
30
|
const dev_1 = require("./commands/dev");
|
|
30
31
|
const ai_init_1 = require("./commands/ai-init");
|
|
31
32
|
const ai_examples_1 = require("./commands/ai-examples");
|
|
@@ -158,8 +159,13 @@ project
|
|
|
158
159
|
.option('-e, --env <environment>', 'Target environment (production/staging/dev). Required on first deploy of a new project.')
|
|
159
160
|
.option('--create', 'Create environment project if it doesn\'t exist')
|
|
160
161
|
.option('--config-only', 'Sync YAML env declarations without building/deploying')
|
|
162
|
+
.option('--no-cache', 'Force a fresh build, bypassing all build caches')
|
|
163
|
+
.option('--force-rebuild', 'Force a fresh build, bypassing all build caches (alias for --no-cache)')
|
|
161
164
|
.action((projectName, path, options) => {
|
|
162
|
-
|
|
165
|
+
// Commander's negation convention maps --no-cache to options.cache === false
|
|
166
|
+
// (NOT options.noCache). Normalize both flags to a single boolean.
|
|
167
|
+
const noCache = options.cache === false || options.forceRebuild === true;
|
|
168
|
+
(0, deploy_1.deploy)(projectName, path, { ...options, noCache });
|
|
163
169
|
});
|
|
164
170
|
project
|
|
165
171
|
.command('pull')
|
|
@@ -173,9 +179,10 @@ project
|
|
|
173
179
|
project
|
|
174
180
|
.command('logs')
|
|
175
181
|
.description('View build/deployment logs for a project')
|
|
176
|
-
.argument('<project>', 'Project name')
|
|
177
|
-
.
|
|
178
|
-
(
|
|
182
|
+
.argument('<project>', 'Project name (or family name with -e)')
|
|
183
|
+
.option('-e, --environment <environment>', 'Environment to resolve (production/staging/dev)')
|
|
184
|
+
.action((projectName, options) => {
|
|
185
|
+
(0, project_logs_1.logsBuild)(projectName, options.environment);
|
|
179
186
|
});
|
|
180
187
|
project
|
|
181
188
|
.command('list')
|
|
@@ -210,6 +217,7 @@ runCmd
|
|
|
210
217
|
.option('--detailed', 'Include timeline, steps, and logs per run (default limit: 5)')
|
|
211
218
|
.option('--has-errors', 'Show only runs with errors (step errors, retries, or degraded results)')
|
|
212
219
|
.option('--json', 'Output as JSON')
|
|
220
|
+
.option('-e, --environment <environment>', 'Environment to filter by (production/staging/dev)')
|
|
213
221
|
.action((projectName, options) => {
|
|
214
222
|
(0, run_list_1.runs)(projectName, options);
|
|
215
223
|
});
|
|
@@ -306,6 +314,7 @@ schedule
|
|
|
306
314
|
.argument('<cron>', 'Cron expression (e.g., "0 9 * * *" for daily at 9am)')
|
|
307
315
|
.option('-w, --workflow <name>', 'Workflow name (if project has multiple)')
|
|
308
316
|
.option('-i, --input <json>', 'JSON input to pass to scheduled runs')
|
|
317
|
+
.option('-z, --timezone <iana>', 'IANA timezone the cron is evaluated in (e.g. America/Chicago); defaults to UTC')
|
|
309
318
|
.option('-y, --yes', 'Skip confirmation if schedule already exists')
|
|
310
319
|
.action((projectName, cron, options) => {
|
|
311
320
|
(0, schedule_set_1.scheduleSet)(projectName, cron, options);
|
|
@@ -340,6 +349,16 @@ webhook
|
|
|
340
349
|
.action((projectName, options) => {
|
|
341
350
|
(0, webhook_list_1.webhookList)(projectName, options);
|
|
342
351
|
});
|
|
352
|
+
webhook
|
|
353
|
+
.command('secret')
|
|
354
|
+
.description('Print the webhook secret for a project (set this value in your sender)')
|
|
355
|
+
.argument('<project>', 'Project name')
|
|
356
|
+
.option('-e, --env <environment>', 'Environment (production/staging/dev)')
|
|
357
|
+
.option('--workflow <name>', 'Filter to a specific workflow by name')
|
|
358
|
+
.option('--format <format>', 'Output format: text or json', 'text')
|
|
359
|
+
.action((projectName, options) => {
|
|
360
|
+
(0, webhook_secret_1.webhookSecret)(projectName, options);
|
|
361
|
+
});
|
|
343
362
|
// =============================================================================
|
|
344
363
|
// workspace <subcommand>
|
|
345
364
|
// =============================================================================
|