@solidactions/cli 1.21.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 +15 -0
- package/dist/commands/env-map.js +5 -0
- package/dist/commands/env-push.js +9 -0
- package/dist/commands/env-set.js +18 -0
- package/dist/commands/init.js +4 -2
- package/dist/commands/login.js +22 -8
- 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/index.js +1 -0
- package/dist/utils/env.js +19 -0
- package/dist/utils/skills.js +19 -0
- package/package.json +1 -1
package/dist/commands/deploy.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.SKILLS_TIP_LINES = void 0;
|
|
6
7
|
exports.cacheBusterEntry = cacheBusterEntry;
|
|
7
8
|
exports.shouldPrintWorkspaceMismatch = shouldPrintWorkspaceMismatch;
|
|
8
9
|
exports.deploy = deploy;
|
|
@@ -19,6 +20,13 @@ const env_1 = require("../utils/env");
|
|
|
19
20
|
const api_1 = require("../utils/api");
|
|
20
21
|
const deploy_ignore_1 = require("../utils/deploy-ignore");
|
|
21
22
|
const slug_1 = require("../utils/slug");
|
|
23
|
+
const skills_1 = require("../utils/skills");
|
|
24
|
+
/** Printed when a deploy target has no SolidActions skill files installed. */
|
|
25
|
+
exports.SKILLS_TIP_LINES = [
|
|
26
|
+
'Tip: no SolidActions skill files found (.claude/skills/solidactions-*). Your AI assistant',
|
|
27
|
+
'works much better with them — run `solidactions ai init --claude` (or --agents) in this',
|
|
28
|
+
'directory to add them.',
|
|
29
|
+
];
|
|
22
30
|
/**
|
|
23
31
|
* Validate project structure before deployment.
|
|
24
32
|
* Checks for required files and SDK dependency.
|
|
@@ -162,6 +170,13 @@ async function deploy(projectName, sourcePath, options = {}) {
|
|
|
162
170
|
console.error(chalk_1.default.red(`Source directory not found: ${sourceDir}`));
|
|
163
171
|
process.exit(1);
|
|
164
172
|
}
|
|
173
|
+
// Non-blocking: AI assistants work measurably better with the skill files
|
|
174
|
+
// (field report: a 600-line skill file cracked the env-scope bug).
|
|
175
|
+
if (!(0, skills_1.hasSolidActionsSkills)(sourceDir)) {
|
|
176
|
+
for (const line of exports.SKILLS_TIP_LINES) {
|
|
177
|
+
console.log(chalk_1.default.yellow(line));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
165
180
|
// -------------------------------------------------------------------------
|
|
166
181
|
// First-deploy check — must happen BEFORE we apply any env default so that
|
|
167
182
|
// first-time deploys without -e get a helpful error prompting a deliberate
|
package/dist/commands/env-map.js
CHANGED
|
@@ -8,8 +8,13 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const prompts_1 = __importDefault(require("prompts"));
|
|
10
10
|
const api_1 = require("../utils/api");
|
|
11
|
+
const env_1 = require("../utils/env");
|
|
11
12
|
async function envMap(projectName, projectKey, globalKey, options = {}) {
|
|
12
13
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
14
|
+
if ((0, env_1.isReservedEnvName)(projectKey)) {
|
|
15
|
+
console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(projectKey)));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
13
18
|
console.log(chalk_1.default.blue(`Mapping global variable "${globalKey}" to project key "${projectKey}" in "${projectName}"...`));
|
|
14
19
|
try {
|
|
15
20
|
// First, get the global variable ID by key
|
|
@@ -72,6 +72,15 @@ async function envPush(projectName, sourcePath, options = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
process.exit(0);
|
|
74
74
|
}
|
|
75
|
+
// Hard-reject reserved names among the keys that would actually be pushed
|
|
76
|
+
// (the server rejects them too — fail here, before any network call).
|
|
77
|
+
const reservedKeys = keysToProcess.filter(env_1.isReservedEnvName);
|
|
78
|
+
if (reservedKeys.length > 0) {
|
|
79
|
+
console.error(chalk_1.default.red(`Refusing to push — reserved ${env_1.RESERVED_ENV_PREFIX} names found: ${reservedKeys.join(', ')}`));
|
|
80
|
+
console.error(chalk_1.default.red('These names are set by the platform at runtime (API credentials, run context) and a custom variable would clobber them, causing authentication failures. ' +
|
|
81
|
+
`Rename them (e.g. "${reservedKeys[0].replace(/^SOLIDACTIONS_/, 'MY_')}") and retry. Nothing was pushed.`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
75
84
|
// Build project slug
|
|
76
85
|
const projectSlug = environment === 'production'
|
|
77
86
|
? projectName
|
package/dist/commands/env-set.js
CHANGED
|
@@ -3,16 +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;
|
|
6
7
|
exports.isNonTty = isNonTty;
|
|
7
8
|
exports.envSet = envSet;
|
|
8
9
|
const axios_1 = __importDefault(require("axios"));
|
|
9
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
11
|
const prompts_1 = __importDefault(require("prompts"));
|
|
11
12
|
const api_1 = require("../utils/api");
|
|
13
|
+
const env_1 = require("../utils/env");
|
|
12
14
|
/** Returns true when stdin is not an interactive terminal (CI, pipes, scripts). */
|
|
13
15
|
function isNonTty() {
|
|
14
16
|
return !process.stdin.isTTY;
|
|
15
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');
|
|
16
25
|
async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
17
26
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
18
27
|
// Detect mode based on arguments
|
|
@@ -22,6 +31,10 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
|
22
31
|
const projectName = keyOrProject;
|
|
23
32
|
const key = valueOrKey;
|
|
24
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
|
+
}
|
|
25
38
|
const environment = options.env || 'dev';
|
|
26
39
|
// Build project slug
|
|
27
40
|
const projectSlug = environment === 'production'
|
|
@@ -93,6 +106,11 @@ async function envSet(keyOrProject, valueOrKey, valueIfProject, options = {}) {
|
|
|
93
106
|
// Global mode: solidactions env set <key> <value>
|
|
94
107
|
const key = keyOrProject;
|
|
95
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));
|
|
96
114
|
const isSecret = options.secret || false;
|
|
97
115
|
// Build the request body with per-environment values
|
|
98
116
|
const body = {
|
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);
|
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
|
}
|
|
@@ -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
|
}
|
package/dist/index.js
CHANGED
|
@@ -314,6 +314,7 @@ schedule
|
|
|
314
314
|
.argument('<cron>', 'Cron expression (e.g., "0 9 * * *" for daily at 9am)')
|
|
315
315
|
.option('-w, --workflow <name>', 'Workflow name (if project has multiple)')
|
|
316
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')
|
|
317
318
|
.option('-y, --yes', 'Skip confirmation if schedule already exists')
|
|
318
319
|
.action((projectName, cron, options) => {
|
|
319
320
|
(0, schedule_set_1.scheduleSet)(projectName, cron, options);
|
package/dist/utils/env.js
CHANGED
|
@@ -3,10 +3,13 @@ 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.RESERVED_ENV_PREFIX = void 0;
|
|
6
7
|
exports.parseEnvFile = parseEnvFile;
|
|
7
8
|
exports.parseYamlEnvVars = parseYamlEnvVars;
|
|
8
9
|
exports.getYamlDeclaredVars = getYamlDeclaredVars;
|
|
9
10
|
exports.loadSolidActionsConfig = loadSolidActionsConfig;
|
|
11
|
+
exports.isReservedEnvName = isReservedEnvName;
|
|
12
|
+
exports.reservedEnvNameError = reservedEnvNameError;
|
|
10
13
|
const fs_1 = __importDefault(require("fs"));
|
|
11
14
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
15
|
/**
|
|
@@ -91,3 +94,19 @@ function loadSolidActionsConfig(yamlPath) {
|
|
|
91
94
|
const content = fs_1.default.readFileSync(yamlPath, 'utf8');
|
|
92
95
|
return js_yaml_1.default.load(content);
|
|
93
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Reserved runtime env-name prefix. SOLIDACTIONS_* names are set by the
|
|
99
|
+
* platform at dispatch time (SOLIDACTIONS_API_KEY, SOLIDACTIONS_API_URL, run
|
|
100
|
+
* context); a custom variable with such a name clobbers the platform
|
|
101
|
+
* credential — the field failure mode is a bare 401 out of
|
|
102
|
+
* InvokeSystemDatabase.init before any workflow code runs. Case-sensitive,
|
|
103
|
+
* matching the server-side rule (App\Support\ReservedEnvNames).
|
|
104
|
+
*/
|
|
105
|
+
exports.RESERVED_ENV_PREFIX = 'SOLIDACTIONS_';
|
|
106
|
+
function isReservedEnvName(key) {
|
|
107
|
+
return key.startsWith(exports.RESERVED_ENV_PREFIX);
|
|
108
|
+
}
|
|
109
|
+
function reservedEnvNameError(key) {
|
|
110
|
+
const suggestion = key.replace(/^SOLIDACTIONS_/, 'MY_');
|
|
111
|
+
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
|
+
}
|
package/dist/utils/skills.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.skillTargetDir = skillTargetDir;
|
|
7
7
|
exports.installSkills = installSkills;
|
|
8
8
|
exports.fetchAiHelperContent = fetchAiHelperContent;
|
|
9
|
+
exports.hasSolidActionsSkills = hasSolidActionsSkills;
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
@@ -58,3 +59,21 @@ async function installSkills(targetDir) {
|
|
|
58
59
|
async function fetchAiHelperContent(targetFile) {
|
|
59
60
|
return (0, github_1.fetchRawFile)(EXAMPLES_OWNER, EXAMPLES_REPO, targetFile);
|
|
60
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* True when the project dir already has SolidActions skill files installed
|
|
64
|
+
* (either AI-helper convention). Used by `deploy` for a non-blocking tip —
|
|
65
|
+
* these files are how AI assistants self-rescue on env-scope/deploy traps.
|
|
66
|
+
*/
|
|
67
|
+
function hasSolidActionsSkills(projectDir) {
|
|
68
|
+
for (const target of ['CLAUDE.md', 'AGENTS.md']) {
|
|
69
|
+
const dir = skillTargetDir(target, projectDir);
|
|
70
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const entries = fs_1.default.readdirSync(dir);
|
|
74
|
+
if (entries.some((f) => f.startsWith('solidactions-') && f.endsWith('.md'))) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|