@solidactions/cli 0.2.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/dev.js +129 -0
- package/dist/commands/env-pull.js +105 -6
- package/dist/index.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ solidactions deploy <project-name> <path>
|
|
|
35
35
|
| `env:list [project]` | List environment variables |
|
|
36
36
|
| `env:delete <key>` | Delete an environment variable |
|
|
37
37
|
| `env:map <project> <key> <global-key>` | Map a global variable to a project |
|
|
38
|
-
| `env:pull <project>` | Pull resolved env vars to a local file |
|
|
38
|
+
| `env:pull <project>` | Pull resolved env vars (including OAuth tokens) to a local file |
|
|
39
39
|
| `schedule:set <project> <cron>` | Set a cron schedule for a workflow |
|
|
40
40
|
| `schedule:list <project>` | List schedules for a project |
|
|
41
41
|
| `schedule:delete <project> <id>` | Delete a schedule |
|
|
@@ -0,0 +1,129 @@
|
|
|
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.dev = dev;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
async function dev(file, options) {
|
|
12
|
+
// Resolve the workflow file path
|
|
13
|
+
const filePath = path_1.default.resolve(file);
|
|
14
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
15
|
+
console.error(chalk_1.default.red(`File not found: ${filePath}`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
// Find the project root (directory containing package.json)
|
|
19
|
+
const projectDir = findProjectRoot(filePath);
|
|
20
|
+
if (!projectDir) {
|
|
21
|
+
console.error(chalk_1.default.red('Could not find package.json in parent directories.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
// Check that @solidactions/sdk is installed
|
|
25
|
+
const sdkPath = path_1.default.join(projectDir, 'node_modules', '@solidactions', 'sdk');
|
|
26
|
+
if (!fs_1.default.existsSync(sdkPath)) {
|
|
27
|
+
console.error(chalk_1.default.red('@solidactions/sdk is not installed in this project.'));
|
|
28
|
+
console.log(chalk_1.default.gray('Run: npm install @solidactions/sdk'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Check that the testing export exists
|
|
32
|
+
const testingPath = path_1.default.join(sdkPath, 'dist', 'src', 'testing');
|
|
33
|
+
if (!fs_1.default.existsSync(testingPath)) {
|
|
34
|
+
console.error(chalk_1.default.red('@solidactions/sdk version does not include testing utilities.'));
|
|
35
|
+
console.log(chalk_1.default.gray('Update to the latest SDK version: npm install @solidactions/sdk@latest'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
// Validate input JSON if provided
|
|
39
|
+
const input = options.input || '{}';
|
|
40
|
+
try {
|
|
41
|
+
JSON.parse(input);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
console.error(chalk_1.default.red('Invalid JSON input. Use -i \'{"key": "value"}\''));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
// Write temp bootstrap file
|
|
48
|
+
const bootstrapPath = path_1.default.join(projectDir, '.solidactions-dev-bootstrap.mjs');
|
|
49
|
+
const bootstrapContent = generateBootstrap(filePath, input);
|
|
50
|
+
try {
|
|
51
|
+
fs_1.default.writeFileSync(bootstrapPath, bootstrapContent);
|
|
52
|
+
console.log(chalk_1.default.blue('SolidActions local dev mode'));
|
|
53
|
+
console.log(chalk_1.default.gray(`File: ${path_1.default.relative(projectDir, filePath)}`));
|
|
54
|
+
console.log(chalk_1.default.gray(`Input: ${input}`));
|
|
55
|
+
console.log(chalk_1.default.gray('---'));
|
|
56
|
+
// Spawn npx tsx with the bootstrap file
|
|
57
|
+
const child = (0, child_process_1.spawn)('npx', ['tsx', bootstrapPath], {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
cwd: projectDir,
|
|
60
|
+
});
|
|
61
|
+
child.on('error', (err) => {
|
|
62
|
+
cleanup(bootstrapPath);
|
|
63
|
+
if (err.code === 'ENOENT') {
|
|
64
|
+
console.error(chalk_1.default.red('npx not found. Make sure Node.js is installed.'));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.error(chalk_1.default.red(`Failed to start: ${err.message}`));
|
|
68
|
+
}
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
child.on('exit', (code) => {
|
|
72
|
+
cleanup(bootstrapPath);
|
|
73
|
+
process.exit(code ?? 1);
|
|
74
|
+
});
|
|
75
|
+
// Also clean up on signals
|
|
76
|
+
process.on('SIGINT', () => {
|
|
77
|
+
cleanup(bootstrapPath);
|
|
78
|
+
child.kill('SIGINT');
|
|
79
|
+
});
|
|
80
|
+
process.on('SIGTERM', () => {
|
|
81
|
+
cleanup(bootstrapPath);
|
|
82
|
+
child.kill('SIGTERM');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
cleanup(bootstrapPath);
|
|
87
|
+
console.error(chalk_1.default.red(`Failed: ${err.message}`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function generateBootstrap(workflowFile, input) {
|
|
92
|
+
// Use forward slashes for the import path (works cross-platform in ESM)
|
|
93
|
+
const importPath = workflowFile.replace(/\\/g, '/');
|
|
94
|
+
return `// Auto-generated by solidactions dev - do not commit
|
|
95
|
+
import { createMockServer } from '@solidactions/sdk/testing';
|
|
96
|
+
|
|
97
|
+
const server = await createMockServer();
|
|
98
|
+
|
|
99
|
+
process.env.SOLIDACTIONS_API_URL = server.baseUrl;
|
|
100
|
+
process.env.SOLIDACTIONS_API_KEY = 'local-dev';
|
|
101
|
+
process.env.WORKFLOW_INPUT = ${JSON.stringify(input)};
|
|
102
|
+
|
|
103
|
+
// Import the workflow file - this triggers SolidActions.run() at module level
|
|
104
|
+
await import(${JSON.stringify(importPath)});
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
function findProjectRoot(startPath) {
|
|
108
|
+
let dir = path_1.default.dirname(startPath);
|
|
109
|
+
for (let i = 0; i < 10; i++) {
|
|
110
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, 'package.json'))) {
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
const parent = path_1.default.dirname(dir);
|
|
114
|
+
if (parent === dir)
|
|
115
|
+
break;
|
|
116
|
+
dir = parent;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function cleanup(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
123
|
+
fs_1.default.unlinkSync(filePath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore cleanup errors
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -42,7 +42,7 @@ async function envPull(projectName, options = {}) {
|
|
|
42
42
|
console.log(chalk_1.default.blue(`Pulling environment variables from "${projectName}" (${environment})...`));
|
|
43
43
|
try {
|
|
44
44
|
// First, check if there are any secrets
|
|
45
|
-
const checkResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings`, {
|
|
45
|
+
const checkResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?resolve_oauth=true`, {
|
|
46
46
|
headers: {
|
|
47
47
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
48
48
|
'Accept': 'application/json',
|
|
@@ -53,9 +53,9 @@ async function envPull(projectName, options = {}) {
|
|
|
53
53
|
console.log(chalk_1.default.yellow('No environment variables found for this project.'));
|
|
54
54
|
process.exit(0);
|
|
55
55
|
}
|
|
56
|
-
// Check for secrets
|
|
56
|
+
// Check for secrets (skip confirmation for --update-oauth since OAuth tokens are always secrets)
|
|
57
57
|
const hasSecrets = mappings.some((m) => m.is_secret);
|
|
58
|
-
if (hasSecrets && !options.yes) {
|
|
58
|
+
if (hasSecrets && !options.yes && !options.updateOauth) {
|
|
59
59
|
console.log(chalk_1.default.yellow('\nThis project contains secret values.'));
|
|
60
60
|
const confirmed = await confirm('This will expose secret values in plain text. Continue?');
|
|
61
61
|
if (!confirmed) {
|
|
@@ -64,14 +64,97 @@ async function envPull(projectName, options = {}) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
// Now fetch with reveal=true to get actual values
|
|
67
|
-
const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?reveal=true`, {
|
|
67
|
+
const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?reveal=true&resolve_oauth=true`, {
|
|
68
68
|
headers: {
|
|
69
69
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
70
70
|
'Accept': 'application/json',
|
|
71
71
|
},
|
|
72
72
|
});
|
|
73
73
|
const variables = response.data || [];
|
|
74
|
-
//
|
|
74
|
+
// --update-oauth: Only pull OAuth tokens and merge into existing .env
|
|
75
|
+
if (options.updateOauth) {
|
|
76
|
+
const oauthVars = variables.filter((v) => v.source_type === 'oauth_connection');
|
|
77
|
+
if (oauthVars.length === 0) {
|
|
78
|
+
console.log(chalk_1.default.yellow('No OAuth variables found for this project.'));
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
// Build OAuth env var names set and their formatted lines
|
|
82
|
+
const oauthKeySet = new Set(oauthVars.map((v) => v.env_name));
|
|
83
|
+
const oauthLines = [];
|
|
84
|
+
for (const variable of oauthVars) {
|
|
85
|
+
const value = variable.resolved_value ?? variable.value;
|
|
86
|
+
if (value === null || value === undefined) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Add expiry comment
|
|
90
|
+
const connName = variable.oauth_connection_name || 'OAuth';
|
|
91
|
+
if (variable.token_expires_at) {
|
|
92
|
+
oauthLines.push(`# OAuth: ${connName} (expires ${variable.token_expires_at})`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
oauthLines.push(`# OAuth: ${connName} (short-lived, re-pull to refresh)`);
|
|
96
|
+
}
|
|
97
|
+
// Format value
|
|
98
|
+
let formattedValue = value;
|
|
99
|
+
if (typeof value === 'string' && (value.includes(' ') || value.includes('"') || value.includes("'") ||
|
|
100
|
+
value.includes('\n') || value.includes('=') || value.includes('#'))) {
|
|
101
|
+
formattedValue = `"${value.replace(/"/g, '\\"')}"`;
|
|
102
|
+
}
|
|
103
|
+
oauthLines.push(`${variable.env_name}=${formattedValue}`);
|
|
104
|
+
}
|
|
105
|
+
if (!fs_1.default.existsSync(outputPath)) {
|
|
106
|
+
// No .env file — create with just OAuth vars
|
|
107
|
+
console.log(chalk_1.default.yellow('No .env file found — creating with OAuth vars only. Run a full env:pull for all variables.'));
|
|
108
|
+
const content = oauthLines.join('\n') + '\n';
|
|
109
|
+
fs_1.default.writeFileSync(outputPath, content);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Merge into existing .env file
|
|
113
|
+
const existingContent = fs_1.default.readFileSync(outputPath, 'utf-8');
|
|
114
|
+
const existingLines = existingContent.split('\n');
|
|
115
|
+
const preservedLines = [];
|
|
116
|
+
for (let i = 0; i < existingLines.length; i++) {
|
|
117
|
+
const line = existingLines[i];
|
|
118
|
+
const nextLine = existingLines[i + 1];
|
|
119
|
+
// Skip OAuth comment lines if the next line is a skipped OAuth var
|
|
120
|
+
if (line.startsWith('# OAuth:') && nextLine) {
|
|
121
|
+
const nextMatch = nextLine.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
122
|
+
if (nextMatch && oauthKeySet.has(nextMatch[1])) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Skip existing OAuth var lines
|
|
127
|
+
const varMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
128
|
+
if (varMatch && oauthKeySet.has(varMatch[1])) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Skip previous "OAuth tokens (updated ...)" header
|
|
132
|
+
if (line.startsWith('# OAuth tokens (updated ')) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
preservedLines.push(line);
|
|
136
|
+
}
|
|
137
|
+
// Strip trailing blank lines from preserved content
|
|
138
|
+
while (preservedLines.length > 0 && preservedLines[preservedLines.length - 1].trim() === '') {
|
|
139
|
+
preservedLines.pop();
|
|
140
|
+
}
|
|
141
|
+
// Append OAuth section
|
|
142
|
+
preservedLines.push('');
|
|
143
|
+
preservedLines.push(`# OAuth tokens (updated ${new Date().toISOString()})`);
|
|
144
|
+
preservedLines.push(...oauthLines);
|
|
145
|
+
preservedLines.push('');
|
|
146
|
+
fs_1.default.writeFileSync(outputPath, preservedLines.join('\n'));
|
|
147
|
+
}
|
|
148
|
+
console.log(chalk_1.default.green(`\n✓ Updated ${oauthVars.length} OAuth token(s) in ${outputFile}`));
|
|
149
|
+
// Print any OAuth warnings
|
|
150
|
+
for (const variable of oauthVars) {
|
|
151
|
+
if (variable.oauth_warning) {
|
|
152
|
+
console.log(chalk_1.default.yellow(` ⚠ ${variable.env_name}: ${variable.oauth_warning}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Full pull: Build .env file content
|
|
75
158
|
const lines = [];
|
|
76
159
|
lines.push(`# Environment variables for ${projectName} (${environment})`);
|
|
77
160
|
lines.push(`# Generated by solidactions env:pull on ${new Date().toISOString()}`);
|
|
@@ -80,12 +163,22 @@ async function envPull(projectName, options = {}) {
|
|
|
80
163
|
let secretCount = 0;
|
|
81
164
|
for (const variable of variables) {
|
|
82
165
|
const key = variable.env_name;
|
|
83
|
-
const value = variable.value;
|
|
166
|
+
const value = variable.resolved_value ?? variable.value;
|
|
84
167
|
if (value === null || value === undefined) {
|
|
85
168
|
// Skip variables with no value
|
|
86
169
|
lines.push(`# ${key}= (no value configured)`);
|
|
87
170
|
continue;
|
|
88
171
|
}
|
|
172
|
+
// Add OAuth expiry comment above OAuth-sourced variables
|
|
173
|
+
if (variable.source_type === 'oauth_connection') {
|
|
174
|
+
const connName = variable.oauth_connection_name || 'OAuth';
|
|
175
|
+
if (variable.token_expires_at) {
|
|
176
|
+
lines.push(`# OAuth: ${connName} (expires ${variable.token_expires_at})`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
lines.push(`# OAuth: ${connName} (short-lived, re-pull to refresh)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
89
182
|
// Quote values that contain special characters
|
|
90
183
|
let formattedValue = value;
|
|
91
184
|
if (typeof value === 'string' && (value.includes(' ') ||
|
|
@@ -110,6 +203,12 @@ async function envPull(projectName, options = {}) {
|
|
|
110
203
|
if (secretCount > 0) {
|
|
111
204
|
console.log(chalk_1.default.yellow(` (includes ${secretCount} secret value${secretCount > 1 ? 's' : ''})`));
|
|
112
205
|
}
|
|
206
|
+
// Print any OAuth warnings
|
|
207
|
+
for (const variable of variables) {
|
|
208
|
+
if (variable.oauth_warning) {
|
|
209
|
+
console.log(chalk_1.default.yellow(` ⚠ ${variable.env_name}: ${variable.oauth_warning}`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
113
212
|
}
|
|
114
213
|
catch (error) {
|
|
115
214
|
if (error.response) {
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const schedule_set_1 = require("./commands/schedule-set");
|
|
|
18
18
|
const schedule_list_1 = require("./commands/schedule-list");
|
|
19
19
|
const schedule_delete_1 = require("./commands/schedule-delete");
|
|
20
20
|
const webhooks_1 = require("./commands/webhooks");
|
|
21
|
+
const dev_1 = require("./commands/dev");
|
|
21
22
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
22
23
|
const pkg = require('../package.json');
|
|
23
24
|
const program = new commander_1.Command();
|
|
@@ -92,6 +93,14 @@ program
|
|
|
92
93
|
.action((project, options) => {
|
|
93
94
|
(0, runs_1.runs)(project, options);
|
|
94
95
|
});
|
|
96
|
+
program
|
|
97
|
+
.command('dev')
|
|
98
|
+
.description('Run a workflow locally using an in-memory mock server (no deploy needed)')
|
|
99
|
+
.argument('<file>', 'Workflow file to run (e.g., src/simple-steps.ts)')
|
|
100
|
+
.option('-i, --input <json>', 'JSON input for the workflow', '{}')
|
|
101
|
+
.action((file, options) => {
|
|
102
|
+
(0, dev_1.dev)(file, options);
|
|
103
|
+
});
|
|
95
104
|
// =============================================================================
|
|
96
105
|
// Logs
|
|
97
106
|
// =============================================================================
|
|
@@ -160,6 +169,7 @@ program
|
|
|
160
169
|
.option('-e, --env <environment>', 'Environment (production/staging/dev)', 'dev')
|
|
161
170
|
.option('-o, --output <file>', 'Output file path (defaults to .env or .env.{environment})')
|
|
162
171
|
.option('-y, --yes', 'Skip confirmation for secrets')
|
|
172
|
+
.option('--update-oauth', 'Only pull OAuth tokens and merge into existing .env file')
|
|
163
173
|
.action((project, options) => {
|
|
164
174
|
(0, env_pull_1.envPull)(project, options);
|
|
165
175
|
});
|