@solidactions/cli 0.4.1 → 0.6.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 CHANGED
@@ -36,6 +36,7 @@ solidactions deploy <project-name> <path>
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
38
  | `env:pull <project>` | Pull resolved env vars (including OAuth tokens) to a local file |
39
+ | `env:push <project> [path]` | Push .env values to a project |
39
40
  | `schedule:set <project> <cron>` | Set a cron schedule for a workflow |
40
41
  | `schedule:list <project>` | List schedules for a project |
41
42
  | `schedule:delete <project> <id>` | Delete a schedule |
@@ -0,0 +1,127 @@
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.aiExamples = aiExamples;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const prompts_1 = __importDefault(require("prompts"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const github_1 = require("../utils/github");
13
+ const markers_1 = require("../utils/markers");
14
+ async function downloadDirectory(owner, repo, remotePath, localPath) {
15
+ const contents = await (0, github_1.listRepoContents)(owner, repo, remotePath);
16
+ fs_extra_1.default.ensureDirSync(localPath);
17
+ for (const item of contents) {
18
+ const localItemPath = path_1.default.join(localPath, item.name);
19
+ if (item.type === 'file') {
20
+ const content = await (0, github_1.fetchRawFile)(owner, repo, `${remotePath}/${item.name}`);
21
+ fs_1.default.writeFileSync(localItemPath, content, 'utf8');
22
+ }
23
+ else if (item.type === 'dir') {
24
+ await downloadDirectory(owner, repo, `${remotePath}/${item.name}`, localItemPath);
25
+ }
26
+ }
27
+ }
28
+ async function aiExamples(names, options = {}) {
29
+ try {
30
+ console.log(chalk_1.default.blue('Discovering available examples...'));
31
+ // Discover available examples
32
+ const repoContents = await (0, github_1.listRepoContents)('SolidActions', 'solidactions-examples');
33
+ const availableExamples = repoContents.filter((item) => item.type === 'dir' && !item.name.startsWith('.'));
34
+ if (availableExamples.length === 0) {
35
+ console.log(chalk_1.default.yellow('No examples found in the repository.'));
36
+ process.exit(0);
37
+ }
38
+ // Determine which to clone
39
+ let selected;
40
+ if (options.all) {
41
+ selected = availableExamples.map((e) => e.name);
42
+ }
43
+ else if (names.length > 0) {
44
+ const availableNames = availableExamples.map((e) => e.name);
45
+ for (const name of names) {
46
+ if (!availableNames.includes(name)) {
47
+ console.error(chalk_1.default.red(`Example "${name}" not found. Available: ${availableNames.join(', ')}`));
48
+ process.exit(1);
49
+ }
50
+ }
51
+ selected = names;
52
+ }
53
+ else {
54
+ const response = await (0, prompts_1.default)({
55
+ type: 'multiselect',
56
+ name: 'selected',
57
+ message: 'Select examples to install',
58
+ choices: availableExamples.map((e) => ({ title: e.name, value: e.name })),
59
+ });
60
+ if (!response.selected || response.selected.length === 0) {
61
+ console.log(chalk_1.default.yellow('No examples selected.'));
62
+ process.exit(0);
63
+ }
64
+ selected = response.selected;
65
+ }
66
+ // Clone each selected example
67
+ const examplesDir = '.solidactions/examples';
68
+ let installedCount = 0;
69
+ for (const name of selected) {
70
+ const localDir = path_1.default.join(examplesDir, name);
71
+ if (fs_1.default.existsSync(localDir) && !options.overwrite) {
72
+ console.log(chalk_1.default.yellow(`Example "${name}" already exists. Use --overwrite to replace.`));
73
+ continue;
74
+ }
75
+ if (fs_1.default.existsSync(localDir) && options.overwrite) {
76
+ fs_extra_1.default.removeSync(localDir);
77
+ }
78
+ console.log(chalk_1.default.gray(`Downloading ${name}...`));
79
+ await downloadDirectory('SolidActions', 'solidactions-examples', name, localDir);
80
+ console.log(chalk_1.default.green(`✓ Installed example: ${name}`));
81
+ installedCount++;
82
+ }
83
+ // Detect AI helper file and upsert examples reference
84
+ const aiHelperFile = (0, markers_1.findAiHelperFile)();
85
+ if (!aiHelperFile) {
86
+ console.log(chalk_1.default.yellow('No AI helper file found. Run "solidactions ai:init" first to create one.'));
87
+ }
88
+ else {
89
+ // List ALL installed examples (existing + newly installed)
90
+ const allInstalled = [];
91
+ if (fs_1.default.existsSync(examplesDir)) {
92
+ const dirs = fs_1.default.readdirSync(examplesDir, { withFileTypes: true });
93
+ for (const dir of dirs) {
94
+ if (dir.isDirectory()) {
95
+ allInstalled.push(dir.name);
96
+ }
97
+ }
98
+ }
99
+ const examplesList = allInstalled.map((name) => `- ${name}`).join('\n');
100
+ const examplesNote = `## SolidActions Examples
101
+
102
+ Example workflows are available in \`.solidactions/examples/\`. Use these as reference patterns when writing SolidActions workflows.
103
+
104
+ Installed examples:
105
+ ${examplesList}
106
+
107
+ When writing workflows, check these examples for patterns covering steps, sleep, signals, child workflows, retries, events, messaging, parallel execution, scheduling, OAuth, streaming, and webhooks.`;
108
+ (0, markers_1.upsertMarkerSection)(aiHelperFile, 'SolidActions Examples', examplesNote);
109
+ }
110
+ console.log(chalk_1.default.green(`✓ Installed ${installedCount} example(s) to .solidactions/examples/`));
111
+ }
112
+ catch (error) {
113
+ if (error.message?.includes('rate limit')) {
114
+ console.error(chalk_1.default.red(error.message));
115
+ }
116
+ else if (error.message?.includes('not found')) {
117
+ console.error(chalk_1.default.red(error.message));
118
+ }
119
+ else if (error.message?.includes('Failed to')) {
120
+ console.error(chalk_1.default.red('Network error: Could not reach GitHub. Check your internet connection.'));
121
+ }
122
+ else {
123
+ console.error(chalk_1.default.red('Error:'), error.message);
124
+ }
125
+ process.exit(1);
126
+ }
127
+ }
@@ -0,0 +1,76 @@
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.aiInit = aiInit;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const prompts_1 = __importDefault(require("prompts"));
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const github_1 = require("../utils/github");
12
+ const markers_1 = require("../utils/markers");
13
+ async function aiInit(options = {}) {
14
+ try {
15
+ // Determine target file
16
+ let targetFile;
17
+ if (options.claude && options.agents) {
18
+ console.error(chalk_1.default.red('Please specify only one of --claude or --agents'));
19
+ process.exit(1);
20
+ }
21
+ if (options.claude) {
22
+ targetFile = 'CLAUDE.md';
23
+ }
24
+ else if (options.agents) {
25
+ targetFile = 'AGENTS.md';
26
+ }
27
+ else {
28
+ const response = await (0, prompts_1.default)({
29
+ type: 'select',
30
+ name: 'file',
31
+ message: 'Which AI helper file?',
32
+ choices: [
33
+ { title: 'CLAUDE.md', value: 'CLAUDE.md' },
34
+ { title: 'AGENTS.md', value: 'AGENTS.md' },
35
+ ],
36
+ });
37
+ if (!response.file) {
38
+ console.log(chalk_1.default.yellow('Cancelled.'));
39
+ process.exit(0);
40
+ }
41
+ targetFile = response.file;
42
+ }
43
+ console.log(chalk_1.default.blue('Fetching AI helper content...'));
44
+ // Fetch AI helper content from examples repo
45
+ const aiContent = await (0, github_1.fetchRawFile)('SolidActions', 'solidactions-examples', 'CLAUDE.md');
46
+ // Fetch SDK reference
47
+ const sdkContent = await (0, github_1.fetchRawFile)('SolidActions', 'solidactions-ts-sdk', 'docs/sdk-reference.md');
48
+ // Save SDK reference to .solidactions/sdk-reference.md
49
+ fs_extra_1.default.ensureDirSync('.solidactions');
50
+ fs_1.default.writeFileSync('.solidactions/sdk-reference.md', sdkContent, 'utf8');
51
+ console.log(chalk_1.default.green('✓ SDK reference saved to .solidactions/sdk-reference.md'));
52
+ // Upsert main AI helper section
53
+ (0, markers_1.upsertMarkerSection)(targetFile, 'SolidActions', aiContent);
54
+ // Upsert SDK reference pointer section
55
+ const sdkReferenceNote = `## SolidActions SDK Reference
56
+
57
+ The full SDK API reference is available at \`.solidactions/sdk-reference.md\`. Refer to it for detailed function signatures, error classes, retry configuration, and advanced patterns like forking, streaming, and signal URLs.`;
58
+ (0, markers_1.upsertMarkerSection)(targetFile, 'SolidActions SDK Reference', sdkReferenceNote);
59
+ console.log(chalk_1.default.green(`✓ AI helper installed to ${targetFile}`));
60
+ }
61
+ catch (error) {
62
+ if (error.message?.includes('rate limit')) {
63
+ console.error(chalk_1.default.red(error.message));
64
+ }
65
+ else if (error.message?.includes('not found')) {
66
+ console.error(chalk_1.default.red(error.message));
67
+ }
68
+ else if (error.message?.includes('Failed to fetch')) {
69
+ console.error(chalk_1.default.red('Network error: Could not reach GitHub. Check your internet connection.'));
70
+ }
71
+ else {
72
+ console.error(chalk_1.default.red('Error:'), error.message);
73
+ }
74
+ process.exit(1);
75
+ }
76
+ }
@@ -12,6 +12,7 @@ const form_data_1 = __importDefault(require("form-data"));
12
12
  const chalk_1 = __importDefault(require("chalk"));
13
13
  const js_yaml_1 = __importDefault(require("js-yaml"));
14
14
  const init_1 = require("./init");
15
+ const env_1 = require("../utils/env");
15
16
  /**
16
17
  * Validate project structure before deployment.
17
18
  * Checks for required files and SDK dependency.
@@ -84,94 +85,12 @@ function validateProject(sourceDir) {
84
85
  }
85
86
  return { valid: errors.length === 0, errors, warnings };
86
87
  }
87
- /**
88
- * Parse a .env file into a key-value map.
89
- */
90
- function parseEnvFile(filePath) {
91
- const envMap = new Map();
92
- if (!fs_1.default.existsSync(filePath)) {
93
- return envMap;
94
- }
95
- const content = fs_1.default.readFileSync(filePath, 'utf8');
96
- const lines = content.split('\n');
97
- for (const line of lines) {
98
- const trimmed = line.trim();
99
- // Skip empty lines and comments
100
- if (!trimmed || trimmed.startsWith('#')) {
101
- continue;
102
- }
103
- const equalsIndex = trimmed.indexOf('=');
104
- if (equalsIndex === -1) {
105
- continue;
106
- }
107
- const key = trimmed.substring(0, equalsIndex).trim();
108
- let value = trimmed.substring(equalsIndex + 1).trim();
109
- // Remove surrounding quotes if present
110
- if ((value.startsWith('"') && value.endsWith('"')) ||
111
- (value.startsWith("'") && value.endsWith("'"))) {
112
- value = value.slice(1, -1);
113
- }
114
- envMap.set(key, value);
115
- }
116
- return envMap;
117
- }
118
- /**
119
- * Parse YAML env declarations into structured format.
120
- * Handles the new simplified format:
121
- * - VAR_NAME -> { key: "VAR_NAME", mappedTo: null }
122
- * - VAR_NAME: GLOBAL -> { key: "VAR_NAME", mappedTo: "GLOBAL" }
123
- */
124
- function parseYamlEnvVars(config) {
125
- const parsedVars = [];
126
- if (!config.env || !Array.isArray(config.env)) {
127
- return parsedVars;
128
- }
129
- for (const item of config.env) {
130
- if (typeof item === 'string') {
131
- // Simple string: - VAR_NAME (declared only, needs configuration)
132
- parsedVars.push({ key: item, mappedTo: null, oauthName: null });
133
- }
134
- else if (typeof item === 'object' && item !== null) {
135
- // Object: - VAR_NAME: GLOBAL_NAME or - VAR_NAME: { oauth: connection-name }
136
- const keys = Object.keys(item);
137
- if (keys.length === 1) {
138
- const key = keys[0];
139
- const value = item[key];
140
- if (typeof value === 'object' && value !== null && 'oauth' in value) {
141
- if (typeof value.oauth !== 'string' || !value.oauth) {
142
- throw new Error(`Invalid env config for ${key}: 'oauth' must be a non-empty string`);
143
- }
144
- parsedVars.push({ key, mappedTo: null, oauthName: value.oauth });
145
- }
146
- else {
147
- parsedVars.push({ key, mappedTo: value || null, oauthName: null });
148
- }
149
- }
150
- }
151
- }
152
- return parsedVars;
153
- }
154
- /**
155
- * Extract declared variable keys from solidactions.yaml env config.
156
- * Returns a set of env var keys that are declared in YAML.
157
- */
158
- function getYamlDeclaredVars(config) {
159
- const parsedVars = parseYamlEnvVars(config);
160
- return new Set(parsedVars.map(v => v.key));
161
- }
162
- /**
163
- * Check if deployEnv is enabled in the config.
164
- * New format has deployEnv at top level.
165
- */
166
- function isDeployEnvEnabled(config) {
167
- return config.deployEnv === true;
168
- }
169
88
  /**
170
89
  * Push YAML env declarations to the project.
171
90
  * This registers all YAML-declared vars and their mappings.
172
91
  */
173
92
  async function pushYamlDeclarations(host, apiKey, projectSlug, yamlConfig) {
174
- const parsedVars = parseYamlEnvVars(yamlConfig);
93
+ const parsedVars = (0, env_1.parseYamlEnvVars)(yamlConfig);
175
94
  if (parsedVars.length === 0) {
176
95
  return;
177
96
  }
@@ -196,71 +115,6 @@ async function pushYamlDeclarations(host, apiKey, projectSlug, yamlConfig) {
196
115
  console.error(chalk_1.default.yellow('Warning: Failed to sync YAML declarations:'), error.response?.data?.message || error.message);
197
116
  }
198
117
  }
199
- /**
200
- * Push environment variables from .env file to project.
201
- * Only pushes vars that are declared in solidactions.yaml.
202
- */
203
- async function pushEnvVars(host, apiKey, projectSlug, sourceDir, environment, yamlConfig) {
204
- // Get vars declared in YAML
205
- const declaredVars = getYamlDeclaredVars(yamlConfig);
206
- if (declaredVars.size === 0) {
207
- console.log(chalk_1.default.gray('No environment variables declared in solidactions.yaml'));
208
- return;
209
- }
210
- // Read the .env file for this environment
211
- const envFileName = environment === 'production' ? '.env' : `.env.${environment}`;
212
- const envFilePath = path_1.default.join(sourceDir, envFileName);
213
- if (!fs_1.default.existsSync(envFilePath)) {
214
- console.log(chalk_1.default.yellow(`⚠ ${envFileName} not found, skipping env push`));
215
- // Warn about each missing declared var
216
- for (const key of declaredVars) {
217
- console.log(chalk_1.default.yellow(` ⚠ ${key}: no value (not in ${envFileName})`));
218
- }
219
- return;
220
- }
221
- // Parse the .env file
222
- const envValues = parseEnvFile(envFilePath);
223
- // Filter to only YAML-declared vars
224
- const variables = [];
225
- const missing = [];
226
- for (const key of declaredVars) {
227
- const value = envValues.get(key);
228
- if (value !== undefined) {
229
- // Mark vars containing "secret", "key", "token", "password" as secrets
230
- const isSecret = /secret|key|token|password|credential/i.test(key);
231
- variables.push({ key, value, is_secret: isSecret });
232
- }
233
- else {
234
- missing.push(key);
235
- }
236
- }
237
- // Warn about missing vars
238
- for (const key of missing) {
239
- console.log(chalk_1.default.yellow(`⚠ ${key}: not found in ${envFileName}`));
240
- }
241
- if (variables.length === 0) {
242
- console.log(chalk_1.default.yellow('No matching environment variables to push'));
243
- return;
244
- }
245
- // Push to API
246
- try {
247
- const response = await axios_1.default.post(`${host}/api/v1/projects/${projectSlug}/variable-mappings/bulk`, { variables }, {
248
- headers: {
249
- 'Authorization': `Bearer ${apiKey}`,
250
- 'Accept': 'application/json',
251
- 'Content-Type': 'application/json',
252
- },
253
- });
254
- const { created, updated } = response.data;
255
- console.log(chalk_1.default.green(`✓ Pushed ${variables.length} environment variables from ${envFileName}`));
256
- if (created > 0 || updated > 0) {
257
- console.log(chalk_1.default.gray(` (${created} created, ${updated} updated)`));
258
- }
259
- }
260
- catch (error) {
261
- console.error(chalk_1.default.red('Failed to push environment variables:'), error.response?.data?.message || error.message);
262
- }
263
- }
264
118
  async function deploy(projectName, sourcePath, options = {}) {
265
119
  const config = (0, init_1.getConfig)();
266
120
  if (!config?.apiKey) {
@@ -399,11 +253,6 @@ async function deploy(projectName, sourcePath, options = {}) {
399
253
  if (yamlConfig) {
400
254
  await pushYamlDeclarations(config.host, config.apiKey, projectSlug, yamlConfig);
401
255
  }
402
- // Push .env values if deployEnv is enabled
403
- if (yamlConfig && isDeployEnvEnabled(yamlConfig)) {
404
- console.log(chalk_1.default.blue('\nPushing environment variables...'));
405
- await pushEnvVars(config.host, config.apiKey, projectSlug, sourceDir, environment, yamlConfig);
406
- }
407
256
  fs_1.default.unlinkSync(zipPath);
408
257
  process.exit(0);
409
258
  }
@@ -0,0 +1,208 @@
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.envPush = envPush;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const axios_1 = __importDefault(require("axios"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const prompts_1 = __importDefault(require("prompts"));
12
+ const init_1 = require("./init");
13
+ const env_1 = require("../utils/env");
14
+ async function envPush(projectName, sourcePath, options = {}) {
15
+ const config = (0, init_1.getConfig)();
16
+ if (!config?.apiKey) {
17
+ console.error(chalk_1.default.red('Not initialized. Run "solidactions init <api-key>" first.'));
18
+ process.exit(1);
19
+ }
20
+ const sourceDir = path_1.default.resolve(sourcePath);
21
+ const environment = options.env || 'dev';
22
+ // Read solidactions.yaml
23
+ const yamlPath = path_1.default.join(sourceDir, 'solidactions.yaml');
24
+ if (!fs_1.default.existsSync(yamlPath)) {
25
+ console.error(chalk_1.default.red('solidactions.yaml not found in ' + sourceDir));
26
+ console.log(chalk_1.default.gray('Make sure you\'re in a SolidActions project directory.'));
27
+ process.exit(1);
28
+ }
29
+ let yamlConfig;
30
+ try {
31
+ yamlConfig = (0, env_1.loadSolidActionsConfig)(yamlPath);
32
+ }
33
+ catch (err) {
34
+ console.error(chalk_1.default.red('Failed to parse solidactions.yaml:'), err.message);
35
+ process.exit(1);
36
+ }
37
+ // Get YAML-declared vars
38
+ const declaredVars = (0, env_1.getYamlDeclaredVars)(yamlConfig);
39
+ // Determine env file
40
+ const envFileName = environment === 'production' ? '.env' : `.env.${environment}`;
41
+ const envFilePath = path_1.default.join(sourceDir, envFileName);
42
+ if (!fs_1.default.existsSync(envFilePath)) {
43
+ console.error(chalk_1.default.red(`${envFileName} not found in ${sourceDir}`));
44
+ console.log(chalk_1.default.gray(`Create ${envFileName} with your environment variables, or use a different --env.`));
45
+ process.exit(1);
46
+ }
47
+ // Parse the .env file
48
+ const envValues = (0, env_1.parseEnvFile)(envFilePath);
49
+ if (envValues.size === 0) {
50
+ console.log(chalk_1.default.yellow(`${envFileName} is empty — nothing to push.`));
51
+ process.exit(0);
52
+ }
53
+ // Filter to YAML-declared vars only (unless --include-undeclared)
54
+ let keysToProcess;
55
+ if (options.includeUndeclared) {
56
+ keysToProcess = Array.from(envValues.keys());
57
+ }
58
+ else {
59
+ if (declaredVars.size === 0) {
60
+ console.log(chalk_1.default.yellow('No environment variables declared in solidactions.yaml.'));
61
+ console.log(chalk_1.default.gray('Use --include-undeclared to push all vars from the .env file.'));
62
+ process.exit(0);
63
+ }
64
+ keysToProcess = Array.from(envValues.keys()).filter(k => declaredVars.has(k));
65
+ // Warn about YAML-declared vars missing from .env
66
+ for (const key of declaredVars) {
67
+ if (!envValues.has(key)) {
68
+ console.log(chalk_1.default.yellow(`⚠ ${key}: declared in YAML but not found in ${envFileName}`));
69
+ }
70
+ }
71
+ }
72
+ if (keysToProcess.length === 0) {
73
+ console.log(chalk_1.default.yellow('No matching variables to push.'));
74
+ if (!options.includeUndeclared && envValues.size > 0) {
75
+ console.log(chalk_1.default.gray('Your .env has variables not declared in solidactions.yaml. Use --include-undeclared to push them.'));
76
+ }
77
+ process.exit(0);
78
+ }
79
+ // Build project slug
80
+ const projectSlug = environment === 'production'
81
+ ? projectName
82
+ : `${projectName}-${environment}`;
83
+ console.log(chalk_1.default.blue(`Pushing env vars to "${projectName}" (${environment})...`));
84
+ // Fetch current server state
85
+ let serverMappings = [];
86
+ try {
87
+ const response = await axios_1.default.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings`, {
88
+ headers: {
89
+ 'Authorization': `Bearer ${config.apiKey}`,
90
+ 'Accept': 'application/json',
91
+ },
92
+ });
93
+ serverMappings = response.data || [];
94
+ }
95
+ catch (error) {
96
+ if (error.response?.status === 401) {
97
+ console.error(chalk_1.default.red('Authentication failed. Run "solidactions init <api-key>" to re-configure.'));
98
+ process.exit(1);
99
+ }
100
+ else if (error.response?.status === 404) {
101
+ console.error(chalk_1.default.red(`Project "${projectSlug}" not found.`));
102
+ console.log(chalk_1.default.gray(`Deploy first with: solidactions deploy ${projectName} --env ${environment}` + (environment !== 'production' ? ' --create' : '')));
103
+ process.exit(1);
104
+ }
105
+ throw error;
106
+ }
107
+ // Build a lookup of server state by env_name
108
+ const serverState = new Map();
109
+ for (const mapping of serverMappings) {
110
+ serverState.set(mapping.env_name, mapping);
111
+ }
112
+ const entries = [];
113
+ for (const key of keysToProcess) {
114
+ const value = envValues.get(key);
115
+ const isSecret = /secret|key|token|password|credential/i.test(key);
116
+ const existing = serverState.get(key);
117
+ let action;
118
+ if (!existing || existing.status === 'not_configured' || existing.has_value === false) {
119
+ action = 'create';
120
+ }
121
+ else {
122
+ action = 'update';
123
+ }
124
+ // If --new-only, skip updates
125
+ if (options.newOnly && action === 'update') {
126
+ action = 'skip';
127
+ }
128
+ entries.push({ key, value, is_secret: isSecret, action });
129
+ }
130
+ const toCreate = entries.filter(e => e.action === 'create');
131
+ const toUpdate = entries.filter(e => e.action === 'update');
132
+ const toSkip = entries.filter(e => e.action === 'skip');
133
+ const toPush = entries.filter(e => e.action !== 'skip');
134
+ if (toPush.length === 0) {
135
+ console.log(chalk_1.default.yellow('Nothing to push — all variables are already configured.'));
136
+ if (toSkip.length > 0) {
137
+ console.log(chalk_1.default.gray(`${toSkip.length} variable(s) skipped (--new-only).`));
138
+ }
139
+ process.exit(0);
140
+ }
141
+ // Display diff table
142
+ console.log('');
143
+ console.log(chalk_1.default.bold(' KEY'.padEnd(36) + 'ACTION'.padEnd(12) + 'NOTES'));
144
+ console.log(chalk_1.default.gray(' ' + '─'.repeat(56)));
145
+ for (const entry of entries) {
146
+ const keyCol = (' ' + entry.key).padEnd(36);
147
+ let actionCol;
148
+ let notesCol;
149
+ switch (entry.action) {
150
+ case 'create':
151
+ actionCol = chalk_1.default.green('create'.padEnd(12));
152
+ notesCol = chalk_1.default.gray('(new)');
153
+ break;
154
+ case 'update':
155
+ actionCol = chalk_1.default.yellow('update'.padEnd(12));
156
+ notesCol = chalk_1.default.gray('(has existing value)');
157
+ break;
158
+ case 'skip':
159
+ actionCol = chalk_1.default.gray('skip'.padEnd(12));
160
+ notesCol = chalk_1.default.gray('(--new-only)');
161
+ break;
162
+ }
163
+ console.log(keyCol + actionCol + notesCol);
164
+ }
165
+ console.log('');
166
+ // Confirm unless --yes
167
+ if (!options.yes) {
168
+ const summary = [];
169
+ if (toCreate.length > 0)
170
+ summary.push(`${toCreate.length} create`);
171
+ if (toUpdate.length > 0)
172
+ summary.push(`${toUpdate.length} update`);
173
+ if (toSkip.length > 0)
174
+ summary.push(`${toSkip.length} skip`);
175
+ const response = await (0, prompts_1.default)({
176
+ type: 'confirm',
177
+ name: 'confirm',
178
+ message: `Push ${toPush.length} variable(s) to ${projectSlug}? (${summary.join(', ')})`,
179
+ initial: true,
180
+ });
181
+ if (!response.confirm) {
182
+ console.log(chalk_1.default.gray('Cancelled.'));
183
+ return;
184
+ }
185
+ }
186
+ // Push via bulk API
187
+ const variables = toPush.map(e => ({
188
+ key: e.key,
189
+ value: e.value,
190
+ is_secret: e.is_secret,
191
+ }));
192
+ try {
193
+ const response = await axios_1.default.post(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings/bulk`, { variables }, {
194
+ headers: {
195
+ 'Authorization': `Bearer ${config.apiKey}`,
196
+ 'Accept': 'application/json',
197
+ 'Content-Type': 'application/json',
198
+ },
199
+ });
200
+ const { created, updated } = response.data;
201
+ console.log(chalk_1.default.green(`\n✓ Pushed ${toPush.length} variable(s) to ${projectSlug}`));
202
+ console.log(chalk_1.default.gray(` ${created} created, ${updated} updated` + (toSkip.length > 0 ? `, ${toSkip.length} skipped` : '')));
203
+ }
204
+ catch (error) {
205
+ console.error(chalk_1.default.red('Failed to push environment variables:'), error.response?.data?.message || error.message);
206
+ process.exit(1);
207
+ }
208
+ }
package/dist/index.js CHANGED
@@ -14,11 +14,14 @@ const env_list_1 = require("./commands/env-list");
14
14
  const env_delete_1 = require("./commands/env-delete");
15
15
  const env_map_1 = require("./commands/env-map");
16
16
  const env_pull_1 = require("./commands/env-pull");
17
+ const env_push_1 = require("./commands/env-push");
17
18
  const schedule_set_1 = require("./commands/schedule-set");
18
19
  const schedule_list_1 = require("./commands/schedule-list");
19
20
  const schedule_delete_1 = require("./commands/schedule-delete");
20
21
  const webhooks_1 = require("./commands/webhooks");
21
22
  const dev_1 = require("./commands/dev");
23
+ const ai_init_1 = require("./commands/ai-init");
24
+ const ai_examples_1 = require("./commands/ai-examples");
22
25
  // eslint-disable-next-line @typescript-eslint/no-var-requires
23
26
  const pkg = require('../package.json');
24
27
  const program = new commander_1.Command();
@@ -173,6 +176,18 @@ program
173
176
  .action((project, options) => {
174
177
  (0, env_pull_1.envPull)(project, options);
175
178
  });
179
+ program
180
+ .command('env:push')
181
+ .description('Push environment variables from .env file to a project')
182
+ .argument('<project>', 'Project name')
183
+ .argument('[path]', 'Source directory with solidactions.yaml and .env file', '.')
184
+ .option('-e, --env <environment>', 'Target environment (production/staging/dev)', 'dev')
185
+ .option('--new-only', 'Only push new or empty variables (skip existing)')
186
+ .option('--include-undeclared', 'Also push vars not declared in solidactions.yaml')
187
+ .option('-y, --yes', 'Skip confirmation prompt')
188
+ .action((project, path, options) => {
189
+ (0, env_push_1.envPush)(project, path, options);
190
+ });
176
191
  // =============================================================================
177
192
  // Scheduling
178
193
  // =============================================================================
@@ -214,4 +229,20 @@ program
214
229
  .action((project, options) => {
215
230
  (0, webhooks_1.webhookList)(project, options);
216
231
  });
232
+ // =============================================================================
233
+ // AI Helper
234
+ // =============================================================================
235
+ program
236
+ .command('ai:init')
237
+ .description('Install AI helper documentation (CLAUDE.md or AGENTS.md) for AI-assisted workflow development')
238
+ .option('--claude', 'Use CLAUDE.md (for Claude Code)')
239
+ .option('--agents', 'Use AGENTS.md (for Cursor, Windsurf, etc.)')
240
+ .action((options) => { (0, ai_init_1.aiInit)(options); });
241
+ program
242
+ .command('ai:examples')
243
+ .description('Install example workflows for AI reference')
244
+ .argument('[names...]', 'Example names to install (omit for interactive selector)')
245
+ .option('--all', 'Install all available examples')
246
+ .option('--overwrite', 'Overwrite existing examples without warning')
247
+ .action((names, options) => { (0, ai_examples_1.aiExamples)(names, options); });
217
248
  program.parse();
@@ -0,0 +1,93 @@
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.parseEnvFile = parseEnvFile;
7
+ exports.parseYamlEnvVars = parseYamlEnvVars;
8
+ exports.getYamlDeclaredVars = getYamlDeclaredVars;
9
+ exports.loadSolidActionsConfig = loadSolidActionsConfig;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const js_yaml_1 = __importDefault(require("js-yaml"));
12
+ /**
13
+ * Parse a .env file into a key-value map.
14
+ */
15
+ function parseEnvFile(filePath) {
16
+ const envMap = new Map();
17
+ if (!fs_1.default.existsSync(filePath)) {
18
+ return envMap;
19
+ }
20
+ const content = fs_1.default.readFileSync(filePath, 'utf8');
21
+ const lines = content.split('\n');
22
+ for (const line of lines) {
23
+ const trimmed = line.trim();
24
+ // Skip empty lines and comments
25
+ if (!trimmed || trimmed.startsWith('#')) {
26
+ continue;
27
+ }
28
+ const equalsIndex = trimmed.indexOf('=');
29
+ if (equalsIndex === -1) {
30
+ continue;
31
+ }
32
+ const key = trimmed.substring(0, equalsIndex).trim();
33
+ let value = trimmed.substring(equalsIndex + 1).trim();
34
+ // Remove surrounding quotes if present
35
+ if ((value.startsWith('"') && value.endsWith('"')) ||
36
+ (value.startsWith("'") && value.endsWith("'"))) {
37
+ value = value.slice(1, -1);
38
+ }
39
+ envMap.set(key, value);
40
+ }
41
+ return envMap;
42
+ }
43
+ /**
44
+ * Parse YAML env declarations into structured format.
45
+ * Handles the new simplified format:
46
+ * - VAR_NAME -> { key: "VAR_NAME", mappedTo: null }
47
+ * - VAR_NAME: GLOBAL -> { key: "VAR_NAME", mappedTo: "GLOBAL" }
48
+ */
49
+ function parseYamlEnvVars(config) {
50
+ const parsedVars = [];
51
+ if (!config.env || !Array.isArray(config.env)) {
52
+ return parsedVars;
53
+ }
54
+ for (const item of config.env) {
55
+ if (typeof item === 'string') {
56
+ // Simple string: - VAR_NAME (declared only, needs configuration)
57
+ parsedVars.push({ key: item, mappedTo: null, oauthName: null });
58
+ }
59
+ else if (typeof item === 'object' && item !== null) {
60
+ // Object: - VAR_NAME: GLOBAL_NAME or - VAR_NAME: { oauth: connection-name }
61
+ const keys = Object.keys(item);
62
+ if (keys.length === 1) {
63
+ const key = keys[0];
64
+ const value = item[key];
65
+ if (typeof value === 'object' && value !== null && 'oauth' in value) {
66
+ if (typeof value.oauth !== 'string' || !value.oauth) {
67
+ throw new Error(`Invalid env config for ${key}: 'oauth' must be a non-empty string`);
68
+ }
69
+ parsedVars.push({ key, mappedTo: null, oauthName: value.oauth });
70
+ }
71
+ else {
72
+ parsedVars.push({ key, mappedTo: value || null, oauthName: null });
73
+ }
74
+ }
75
+ }
76
+ }
77
+ return parsedVars;
78
+ }
79
+ /**
80
+ * Extract declared variable keys from solidactions.yaml env config.
81
+ * Returns a set of env var keys that are declared in YAML.
82
+ */
83
+ function getYamlDeclaredVars(config) {
84
+ const parsedVars = parseYamlEnvVars(config);
85
+ return new Set(parsedVars.map(v => v.key));
86
+ }
87
+ /**
88
+ * Load and parse a solidactions.yaml config file.
89
+ */
90
+ function loadSolidActionsConfig(yamlPath) {
91
+ const content = fs_1.default.readFileSync(yamlPath, 'utf8');
92
+ return js_yaml_1.default.load(content);
93
+ }
@@ -0,0 +1,41 @@
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.fetchRawFile = fetchRawFile;
7
+ exports.listRepoContents = listRepoContents;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ async function fetchRawFile(owner, repo, path, branch = 'main') {
10
+ try {
11
+ const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
12
+ const response = await axios_1.default.get(url, { responseType: 'text' });
13
+ return response.data;
14
+ }
15
+ catch (error) {
16
+ if (error.response?.status === 404) {
17
+ throw new Error(`File not found: ${owner}/${repo}/${path} (branch: ${branch})`);
18
+ }
19
+ throw new Error(`Failed to fetch ${owner}/${repo}/${path}: ${error.message}`);
20
+ }
21
+ }
22
+ async function listRepoContents(owner, repo, path = '', branch = 'main') {
23
+ try {
24
+ const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`;
25
+ const response = await axios_1.default.get(url);
26
+ return response.data.map((item) => ({
27
+ name: item.name,
28
+ type: item.type,
29
+ download_url: item.download_url,
30
+ }));
31
+ }
32
+ catch (error) {
33
+ if (error.response?.status === 403) {
34
+ throw new Error('GitHub API rate limit reached (60 requests/hour for unauthenticated access). Please wait a few minutes and try again.');
35
+ }
36
+ if (error.response?.status === 404) {
37
+ throw new Error(`Repository or path not found: ${owner}/${repo}/${path}`);
38
+ }
39
+ throw new Error(`Failed to list ${owner}/${repo}/${path}: ${error.message}`);
40
+ }
41
+ }
@@ -0,0 +1,45 @@
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.upsertMarkerSection = upsertMarkerSection;
7
+ exports.hasMarkerSection = hasMarkerSection;
8
+ exports.findAiHelperFile = findAiHelperFile;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ function upsertMarkerSection(filePath, markerName, content) {
13
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(filePath));
14
+ let existing = '';
15
+ if (fs_1.default.existsSync(filePath)) {
16
+ existing = fs_1.default.readFileSync(filePath, 'utf8');
17
+ }
18
+ const openTag = `<!-- ${markerName} -->`;
19
+ const closeTag = `<!-- /${markerName} -->`;
20
+ const block = `${openTag}\n${content}\n${closeTag}`;
21
+ const openIdx = existing.indexOf(openTag);
22
+ const closeIdx = existing.indexOf(closeTag);
23
+ if (openIdx !== -1 && closeIdx !== -1) {
24
+ const before = existing.substring(0, openIdx);
25
+ const after = existing.substring(closeIdx + closeTag.length);
26
+ fs_1.default.writeFileSync(filePath, before + block + after, 'utf8');
27
+ }
28
+ else {
29
+ const result = existing.length > 0 ? existing + '\n\n' + block + '\n' : block + '\n';
30
+ fs_1.default.writeFileSync(filePath, result, 'utf8');
31
+ }
32
+ }
33
+ function hasMarkerSection(filePath, markerName) {
34
+ if (!fs_1.default.existsSync(filePath))
35
+ return false;
36
+ const content = fs_1.default.readFileSync(filePath, 'utf8');
37
+ return content.includes(`<!-- ${markerName} -->`) && content.includes(`<!-- /${markerName} -->`);
38
+ }
39
+ function findAiHelperFile() {
40
+ if (hasMarkerSection('CLAUDE.md', 'SolidActions'))
41
+ return 'CLAUDE.md';
42
+ if (hasMarkerSection('AGENTS.md', 'SolidActions'))
43
+ return 'AGENTS.md';
44
+ return null;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {