@solidactions/cli 1.2.0 → 1.5.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 +51 -1
- package/dist/commands/ai-init.js +40 -3
- package/dist/commands/deploy.js +65 -13
- package/dist/commands/init.js +135 -37
- package/dist/commands/workspaces.js +11 -6
- package/dist/index.js +41 -6
- package/dist/utils/api.js +24 -33
- package/dist/utils/config.js +138 -0
- package/dist/utils/skills.js +71 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,48 @@ solidactions run start <project> <workflow> # Trigger a workflow
|
|
|
18
18
|
solidactions run view <run-id> # Inspect a run
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
The CLI stores `host`, `apiKey`, and `workspaceId` in a JSON config file. Two locations are supported:
|
|
24
|
+
|
|
25
|
+
- **Global** — `~/.solidactions/config.json`. Used by default, shared across all folders for a single user.
|
|
26
|
+
- **Local** — `./.solidactions/config.json`. Scoped to a project folder; takes precedence over the global file when running commands from inside that folder (or any subdirectory — the CLI walks up looking for it).
|
|
27
|
+
|
|
28
|
+
### Resolution order
|
|
29
|
+
|
|
30
|
+
For each field (`host`, `apiKey`, `workspaceId`), the CLI resolves independently in this order:
|
|
31
|
+
|
|
32
|
+
1. Environment variables: `SOLIDACTIONS_HOST`, `SOLIDACTIONS_API_KEY`, `SOLIDACTIONS_WORKSPACE_ID`
|
|
33
|
+
2. Nearest local `./.solidactions/config.json` (walking up from cwd)
|
|
34
|
+
3. Global `~/.solidactions/config.json`
|
|
35
|
+
|
|
36
|
+
You can mix: e.g., set only `SOLIDACTIONS_WORKSPACE_ID` in the environment while letting `host` and `apiKey` come from a file.
|
|
37
|
+
|
|
38
|
+
### `solidactions init` flags
|
|
39
|
+
|
|
40
|
+
- `--local` — write config to `./.solidactions/config.json` in the current folder.
|
|
41
|
+
- `--global` — write config to `~/.solidactions/config.json` (today's default).
|
|
42
|
+
- `--gitignore` — with `--local`, auto-add `.solidactions/` to `.gitignore` without prompting.
|
|
43
|
+
|
|
44
|
+
In interactive shells, `init` without `--local`/`--global` prompts for a location. In non-interactive contexts, one of the flags is required.
|
|
45
|
+
|
|
46
|
+
### `solidactions logout` flags
|
|
47
|
+
|
|
48
|
+
- `--local` — remove only the nearest local config (walks up from cwd).
|
|
49
|
+
- `--global` — remove only the global config.
|
|
50
|
+
- Bare `logout` — removes the nearest local if present, otherwise removes global.
|
|
51
|
+
|
|
52
|
+
### Debugging resolution
|
|
53
|
+
|
|
54
|
+
Set `SOLIDACTIONS_DEBUG=1` on any command to print the resolved configuration and per-field sources to stderr before the command runs. `solidactions whoami` also shows this information.
|
|
55
|
+
|
|
56
|
+
### Use case: multiple AI agents in parallel
|
|
57
|
+
|
|
58
|
+
If you run multiple AI coding agents in different project folders simultaneously, either:
|
|
59
|
+
|
|
60
|
+
- Run `solidactions init <key> --local` in each folder so each has its own config, or
|
|
61
|
+
- Set `SOLIDACTIONS_API_KEY` / `SOLIDACTIONS_WORKSPACE_ID` in the environment each agent uses (no files to share or stomp).
|
|
62
|
+
|
|
21
63
|
## Commands
|
|
22
64
|
|
|
23
65
|
Use `solidactions <command> --help` for full flag details on any command.
|
|
@@ -85,9 +127,17 @@ Use `solidactions <command> --help` for full flag details on any command.
|
|
|
85
127
|
|
|
86
128
|
| Command | Key Flags | Description |
|
|
87
129
|
|---------|-----------|-------------|
|
|
88
|
-
| `ai init` | `--claude`, `--agents` | Install AI helper docs |
|
|
130
|
+
| `ai init` | `--claude`, `--agents`, `--no-skills` | Install AI helper docs |
|
|
89
131
|
| `ai examples [names...]` | `--all`, `--overwrite` | Install example workflows |
|
|
90
132
|
|
|
133
|
+
By default, `ai init` also installs three lazy-loaded SolidActions skills
|
|
134
|
+
into `.claude/skills/`. These activate automatically when you scaffold a
|
|
135
|
+
project, write workflow code, or deploy. The CLAUDE.md injection becomes
|
|
136
|
+
a slim pointer to keep always-loaded context light.
|
|
137
|
+
|
|
138
|
+
Pass `--no-skills` to skip the skill install and get the legacy full
|
|
139
|
+
CLAUDE.md content instead.
|
|
140
|
+
|
|
91
141
|
## Development
|
|
92
142
|
|
|
93
143
|
```bash
|
package/dist/commands/ai-init.js
CHANGED
|
@@ -8,8 +8,10 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const prompts_1 = __importDefault(require("prompts"));
|
|
10
10
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const github_1 = require("../utils/github");
|
|
12
13
|
const markers_1 = require("../utils/markers");
|
|
14
|
+
const skills_1 = require("../utils/skills");
|
|
13
15
|
async function aiInit(options = {}) {
|
|
14
16
|
try {
|
|
15
17
|
// Determine target file
|
|
@@ -40,15 +42,50 @@ async function aiInit(options = {}) {
|
|
|
40
42
|
}
|
|
41
43
|
targetFile = response.file;
|
|
42
44
|
}
|
|
45
|
+
// Decide whether to install skills (default true; --no-skills opts out)
|
|
46
|
+
const installSkillsEnabled = options.skills !== false;
|
|
47
|
+
// Determine skill targets if installing.
|
|
48
|
+
// Skills are Claude Code-specific (.claude/skills/) and not meaningful for other AI tools.
|
|
49
|
+
let skillTargets = [];
|
|
50
|
+
if (installSkillsEnabled && targetFile === 'CLAUDE.md') {
|
|
51
|
+
skillTargets = (0, skills_1.detectSkillTargets)();
|
|
52
|
+
if (skillTargets.length === 0) {
|
|
53
|
+
const resp = await (0, prompts_1.default)({
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
name: 'create',
|
|
56
|
+
message: 'No `.claude/` directory found. Create `.claude/skills/` for SolidActions skills?',
|
|
57
|
+
initial: true,
|
|
58
|
+
});
|
|
59
|
+
if (resp.create === undefined) {
|
|
60
|
+
// User cancelled (Ctrl+C) — treat as abort, not decline
|
|
61
|
+
console.log(chalk_1.default.yellow('Cancelled.'));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
else if (resp.create) {
|
|
65
|
+
skillTargets = [path_1.default.join(process.cwd(), '.claude', 'skills')];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(chalk_1.default.yellow('Skipping skill install. Run with --no-skills to silence this prompt.'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
43
72
|
console.log(chalk_1.default.blue('Fetching AI helper content...'));
|
|
44
|
-
// Fetch
|
|
45
|
-
const aiContent = await (0,
|
|
46
|
-
// Fetch SDK reference
|
|
73
|
+
// Fetch CLAUDE.md content (slim pointer if skills installed, else full)
|
|
74
|
+
const aiContent = await (0, skills_1.fetchClaudeMdContent)(skillTargets.length > 0);
|
|
75
|
+
// Fetch SDK reference (always)
|
|
47
76
|
const sdkContent = await (0, github_1.fetchRawFile)('SolidActions', 'solidactions-ts-sdk', 'docs/sdk-reference.md');
|
|
48
77
|
// Save SDK reference to .solidactions/sdk-reference.md
|
|
49
78
|
fs_extra_1.default.ensureDirSync('.solidactions');
|
|
50
79
|
fs_1.default.writeFileSync('.solidactions/sdk-reference.md', sdkContent, 'utf8');
|
|
51
80
|
console.log(chalk_1.default.green('✓ SDK reference saved to .solidactions/sdk-reference.md'));
|
|
81
|
+
// Install skills if enabled and targets exist
|
|
82
|
+
if (skillTargets.length > 0) {
|
|
83
|
+
console.log(chalk_1.default.blue('Installing SolidActions skills...'));
|
|
84
|
+
const { written } = await (0, skills_1.installSkills)(skillTargets);
|
|
85
|
+
for (const f of written) {
|
|
86
|
+
console.log(chalk_1.default.green(`✓ ${path_1.default.relative(process.cwd(), f)}`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
52
89
|
// Upsert main AI helper section
|
|
53
90
|
(0, markers_1.upsertMarkerSection)(targetFile, 'SolidActions', aiContent);
|
|
54
91
|
// Upsert SDK reference pointer section
|
package/dist/commands/deploy.js
CHANGED
|
@@ -114,11 +114,51 @@ async function pushYamlDeclarations(config, projectSlug, yamlConfig) {
|
|
|
114
114
|
async function deploy(projectName, sourcePath, options = {}) {
|
|
115
115
|
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
116
116
|
const sourceDir = sourcePath ? path_1.default.resolve(sourcePath) : process.cwd();
|
|
117
|
-
|
|
117
|
+
// Capture whether -e was explicitly passed by the caller. Commander leaves
|
|
118
|
+
// options.env as undefined when no default is set and the flag is omitted.
|
|
119
|
+
const explicitEnv = options.env;
|
|
118
120
|
if (!fs_1.default.existsSync(sourceDir)) {
|
|
119
121
|
console.error(chalk_1.default.red(`Source directory not found: ${sourceDir}`));
|
|
120
122
|
process.exit(1);
|
|
121
123
|
}
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
// Production-existence check — must happen BEFORE we apply any env default
|
|
126
|
+
// so that first-time deploys without -e get a helpful error instead of
|
|
127
|
+
// silently creating a dev-only project with no production root.
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
let productionExists = null; // null = unknown (error path)
|
|
130
|
+
let productionSlug = null;
|
|
131
|
+
try {
|
|
132
|
+
const prodResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${projectName}`, {
|
|
133
|
+
headers: (0, api_1.getApiHeaders)(config),
|
|
134
|
+
});
|
|
135
|
+
productionExists = true;
|
|
136
|
+
productionSlug = prodResponse.data.slug || prodResponse.data.name;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.response?.status === 404) {
|
|
140
|
+
productionExists = false;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// 5xx, network error, auth failure, etc. — fail conservatively rather
|
|
144
|
+
// than treating the project as non-existent and potentially creating it.
|
|
145
|
+
console.error(chalk_1.default.red('Failed to check project existence:'), error.response?.data?.message || error.message);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Decision: if production does not exist and no -e flag was given, the user
|
|
150
|
+
// must pick an environment explicitly to avoid creating a broken dev-only project.
|
|
151
|
+
if (productionExists === false && explicitEnv === undefined) {
|
|
152
|
+
console.error(chalk_1.default.red(`\nThis is the first deploy of "${projectName}" — please pick an environment explicitly:\n`));
|
|
153
|
+
console.error(` solidactions project deploy ${projectName} <path> -e production # most projects start here`);
|
|
154
|
+
console.error(` solidactions project deploy ${projectName} <path> -e dev # dev-only (uncommon; no production root will exist)`);
|
|
155
|
+
console.error(` solidactions project deploy ${projectName} <path> -e staging # staging-only (uncommon)`);
|
|
156
|
+
console.error(chalk_1.default.gray('\nTip: most projects should start with `-e production`. A project needs a production root before dev/staging children can attach to it.'));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
// Apply default: if production already exists and the user didn't pass -e,
|
|
160
|
+
// use 'dev' — this preserves existing behavior for mature projects.
|
|
161
|
+
const environment = explicitEnv ?? 'dev';
|
|
122
162
|
const envLabel = environment !== 'production' ? ` (${environment})` : '';
|
|
123
163
|
console.log(chalk_1.default.blue(`Deploying to project "${projectName}"${envLabel}...`));
|
|
124
164
|
console.log(chalk_1.default.gray(`Source: ${sourceDir}`));
|
|
@@ -149,24 +189,36 @@ async function deploy(projectName, sourcePath, options = {}) {
|
|
|
149
189
|
process.exit(1);
|
|
150
190
|
}
|
|
151
191
|
console.log(chalk_1.default.green('✓ Project structure validated'));
|
|
152
|
-
// Check if project exists
|
|
192
|
+
// Check if the target environment's project record exists; create if needed.
|
|
193
|
+
// For production deploys where we already confirmed existence above, reuse
|
|
194
|
+
// the cached slug to avoid a duplicate GET.
|
|
153
195
|
let projectSlug = projectName;
|
|
154
196
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
if (environment === 'production' && productionExists === true && productionSlug !== null) {
|
|
198
|
+
// Already confirmed — skip the re-lookup.
|
|
199
|
+
projectSlug = productionSlug;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// For non-production environments, append the environment to the slug for lookup
|
|
203
|
+
const lookupSlug = environment === 'production'
|
|
204
|
+
? projectName
|
|
205
|
+
: `${projectName}-${environment}`;
|
|
206
|
+
const checkResponse = await axios_1.default.get(`${config.host}/api/v1/projects/${lookupSlug}`, {
|
|
207
|
+
headers: (0, api_1.getApiHeaders)(config),
|
|
208
|
+
});
|
|
209
|
+
projectSlug = checkResponse.data.slug || checkResponse.data.name;
|
|
210
|
+
}
|
|
163
211
|
}
|
|
164
212
|
catch (error) {
|
|
165
213
|
if (error.response?.status === 404) {
|
|
166
|
-
// For non-production environments,
|
|
214
|
+
// For non-production environments, require --create or give a clear hint
|
|
167
215
|
if (environment !== 'production' && !options.create) {
|
|
168
|
-
console.
|
|
169
|
-
console.
|
|
216
|
+
console.error(chalk_1.default.red(`\nProject "${projectName}" doesn't have a ${environment} environment.\n`));
|
|
217
|
+
console.error(` If production is the intended target:`);
|
|
218
|
+
console.error(` solidactions project deploy ${projectName} <path> -e production`);
|
|
219
|
+
console.error('');
|
|
220
|
+
console.error(` If you really want a new ${environment} environment:`);
|
|
221
|
+
console.error(` solidactions project deploy ${projectName} <path> -e ${environment} --create`);
|
|
170
222
|
process.exit(1);
|
|
171
223
|
}
|
|
172
224
|
console.log(chalk_1.default.yellow(`Project "${projectName}"${envLabel} not found. Creating...`));
|
package/dist/commands/init.js
CHANGED
|
@@ -11,41 +11,83 @@ exports.logout = logout;
|
|
|
11
11
|
exports.whoami = whoami;
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
-
const
|
|
14
|
+
const readline_1 = __importDefault(require("readline"));
|
|
15
15
|
const chalk_1 = __importDefault(require("chalk"));
|
|
16
16
|
const api_1 = require("../utils/api");
|
|
17
17
|
const workspaces_1 = require("./workspaces");
|
|
18
|
-
const
|
|
19
|
-
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
18
|
+
const config_1 = require("../utils/config");
|
|
20
19
|
function getConfig() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const resolved = (0, config_1.resolveConfig)();
|
|
21
|
+
return resolved ? resolved.config : null;
|
|
22
|
+
}
|
|
23
|
+
function saveConfig(config) {
|
|
24
|
+
const resolved = (0, config_1.resolveConfig)();
|
|
25
|
+
const targetPath = resolved ? resolved.activePath : (0, config_1.getGlobalConfigPath)();
|
|
26
|
+
(0, config_1.writeConfigFile)(targetPath, config);
|
|
27
|
+
}
|
|
28
|
+
function clearConfig() {
|
|
29
|
+
(0, config_1.removeConfigFile)((0, config_1.getGlobalConfigPath)());
|
|
30
|
+
}
|
|
31
|
+
async function promptLocation() {
|
|
32
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
24
33
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
while (true) {
|
|
35
|
+
const answer = await new Promise((resolve) => {
|
|
36
|
+
rl.question(chalk_1.default.blue('Save config locally (./.solidactions) or globally (~/.solidactions)? [global] '), resolve);
|
|
37
|
+
});
|
|
38
|
+
const normalized = answer.trim().toLowerCase();
|
|
39
|
+
if (normalized === '' || normalized === 'global' || normalized === 'g')
|
|
40
|
+
return 'global';
|
|
41
|
+
if (normalized === 'local' || normalized === 'l')
|
|
42
|
+
return 'local';
|
|
43
|
+
console.log(chalk_1.default.yellow("Please answer 'local' or 'global' (or press Enter for global)."));
|
|
29
44
|
}
|
|
30
|
-
return config;
|
|
31
45
|
}
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
finally {
|
|
47
|
+
rl.close();
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
async function ensureGitignoreCovers(targetDir, auto) {
|
|
51
|
+
const gitignorePath = path_1.default.join(targetDir, '.gitignore');
|
|
52
|
+
const patternToAdd = '.solidactions/';
|
|
53
|
+
let existing = '';
|
|
54
|
+
if (fs_1.default.existsSync(gitignorePath)) {
|
|
55
|
+
existing = fs_1.default.readFileSync(gitignorePath, 'utf-8');
|
|
56
|
+
const lines = existing.split('\n').map((l) => l.trim());
|
|
57
|
+
const isCovered = lines.some((line) => {
|
|
58
|
+
const normalized = line
|
|
59
|
+
.replace(/^\*\*\//, '')
|
|
60
|
+
.replace(/^\//, '')
|
|
61
|
+
.replace(/\/(\*\*|\*)?$/, '');
|
|
62
|
+
return normalized === '.solidactions';
|
|
63
|
+
});
|
|
64
|
+
if (isCovered) {
|
|
65
|
+
return; // already covered
|
|
66
|
+
}
|
|
39
67
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
let shouldAdd = auto;
|
|
69
|
+
if (!shouldAdd && process.stdin.isTTY) {
|
|
70
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
71
|
+
const answer = await new Promise((resolve) => {
|
|
72
|
+
rl.question(chalk_1.default.yellow(`Local config contains an API key. Add \`.solidactions/\` to ${gitignorePath}? [Y/n] `), resolve);
|
|
73
|
+
});
|
|
74
|
+
rl.close();
|
|
75
|
+
shouldAdd = !(answer.trim().toLowerCase().startsWith('n'));
|
|
76
|
+
}
|
|
77
|
+
if (!shouldAdd) {
|
|
78
|
+
console.log(chalk_1.default.yellow(`Skipping .gitignore update. Remember: ${path_1.default.join(targetDir, '.solidactions', 'config.json')} contains your API key.`));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
82
|
+
try {
|
|
83
|
+
fs_1.default.writeFileSync(gitignorePath, `${existing}${prefix}${patternToAdd}\n`);
|
|
84
|
+
console.log(chalk_1.default.green(`Added \`${patternToAdd}\` to ${gitignorePath}.`));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.log(chalk_1.default.yellow(`Could not update ${gitignorePath}: ${err.message}. Add \`.solidactions/\` to it manually — ${path_1.default.join(targetDir, '.solidactions', 'config.json')} contains your API key.`));
|
|
45
88
|
}
|
|
46
89
|
}
|
|
47
90
|
async function init(apiKey, options) {
|
|
48
|
-
// Determine host
|
|
49
91
|
let host;
|
|
50
92
|
if (options.host) {
|
|
51
93
|
host = options.host;
|
|
@@ -56,24 +98,48 @@ async function init(apiKey, options) {
|
|
|
56
98
|
else {
|
|
57
99
|
host = 'https://app.solidactions.com';
|
|
58
100
|
}
|
|
59
|
-
// Validate API key format (should be a Sanctum token)
|
|
60
101
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
61
102
|
console.error(chalk_1.default.red('Error: API key is required.'));
|
|
62
103
|
console.log(chalk_1.default.gray('Generate an API key at: ') + chalk_1.default.blue(`${host}/settings/api-keys`));
|
|
63
104
|
process.exit(1);
|
|
64
105
|
}
|
|
106
|
+
// Determine target location.
|
|
107
|
+
let target;
|
|
108
|
+
if (options.local && options.global) {
|
|
109
|
+
console.error(chalk_1.default.red('Error: --local and --global are mutually exclusive.'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
else if (options.local) {
|
|
113
|
+
target = 'local';
|
|
114
|
+
}
|
|
115
|
+
else if (options.global) {
|
|
116
|
+
target = 'global';
|
|
117
|
+
}
|
|
118
|
+
else if (process.stdin.isTTY) {
|
|
119
|
+
target = await promptLocation();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.error(chalk_1.default.red('Refusing to init non-interactively. Pass --local or --global.'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const targetPath = target === 'local' ? (0, config_1.getLocalConfigPath)() : (0, config_1.getGlobalConfigPath)();
|
|
65
126
|
console.log(chalk_1.default.blue(`Initializing SolidActions CLI...`));
|
|
66
127
|
console.log(chalk_1.default.gray(`Host: ${host}`));
|
|
67
|
-
|
|
128
|
+
if ((0, config_1.readConfigFile)(targetPath)) {
|
|
129
|
+
console.log(chalk_1.default.yellow(`Existing config at ${targetPath} will be overwritten.`));
|
|
130
|
+
}
|
|
68
131
|
const config = {
|
|
69
132
|
host,
|
|
70
133
|
apiKey: apiKey.trim(),
|
|
71
134
|
};
|
|
72
|
-
|
|
135
|
+
(0, config_1.writeConfigFile)(targetPath, config);
|
|
136
|
+
if (target === 'local') {
|
|
137
|
+
await ensureGitignoreCovers(process.cwd(), !!options.gitignore);
|
|
138
|
+
}
|
|
73
139
|
console.log(chalk_1.default.green('CLI initialized successfully!'));
|
|
74
|
-
console.log(chalk_1.default.gray(`Configuration saved to ${
|
|
140
|
+
console.log(chalk_1.default.gray(`Configuration saved to ${targetPath}`));
|
|
75
141
|
console.log('');
|
|
76
|
-
//
|
|
142
|
+
// Workspace selection — ensureWorkspaceSelected writes to the right file via the resolver.
|
|
77
143
|
try {
|
|
78
144
|
if (options.workspace) {
|
|
79
145
|
await (0, workspaces_1.workspaceSet)(options.workspace);
|
|
@@ -91,23 +157,55 @@ async function init(apiKey, options) {
|
|
|
91
157
|
console.log(chalk_1.default.gray(' solidactions run start <proj> <wf> Run a workflow'));
|
|
92
158
|
console.log(chalk_1.default.gray(' solidactions run list List recent runs'));
|
|
93
159
|
}
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
function logout(options = {}) {
|
|
161
|
+
if (options.local && options.global) {
|
|
162
|
+
console.error(chalk_1.default.red('Error: --local and --global are mutually exclusive.'));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const globalPath = (0, config_1.getGlobalConfigPath)();
|
|
166
|
+
const localPath = (0, config_1.findLocalConfigPath)(process.cwd());
|
|
167
|
+
let targetPath;
|
|
168
|
+
if (options.local) {
|
|
169
|
+
targetPath = localPath;
|
|
170
|
+
if (!targetPath) {
|
|
171
|
+
console.error(chalk_1.default.red(`No local config found in ${process.cwd()} or any parent directory.`));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (options.global) {
|
|
176
|
+
targetPath = globalPath;
|
|
98
177
|
}
|
|
99
178
|
else {
|
|
100
|
-
|
|
179
|
+
targetPath = localPath ?? globalPath;
|
|
180
|
+
}
|
|
181
|
+
const removed = (0, config_1.removeConfigFile)(targetPath);
|
|
182
|
+
if (removed) {
|
|
183
|
+
console.log(chalk_1.default.green(`Logged out. Removed ${targetPath}`));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(chalk_1.default.gray(`Not logged in (no config at ${targetPath}).`));
|
|
101
187
|
}
|
|
102
188
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
if (!config) {
|
|
189
|
+
function whoami() {
|
|
190
|
+
const resolved = (0, config_1.resolveConfig)();
|
|
191
|
+
if (!resolved || !resolved.config.apiKey) {
|
|
106
192
|
console.log(chalk_1.default.yellow('Not initialized.'));
|
|
107
193
|
console.log(chalk_1.default.gray('Run "solidactions init <api-key>" to configure.'));
|
|
108
194
|
process.exit(1);
|
|
109
195
|
}
|
|
196
|
+
const { config, sources } = resolved;
|
|
197
|
+
const maskedKey = config.apiKey.length > 12
|
|
198
|
+
? `${config.apiKey.substring(0, 8)}...${config.apiKey.slice(-4)}`
|
|
199
|
+
: config.apiKey;
|
|
200
|
+
const fmt = (src) => {
|
|
201
|
+
if (src === 'env')
|
|
202
|
+
return chalk_1.default.gray('(from $SOLIDACTIONS_* env var)');
|
|
203
|
+
if (src === null)
|
|
204
|
+
return chalk_1.default.gray('(unset)');
|
|
205
|
+
return chalk_1.default.gray(`(from ${src})`);
|
|
206
|
+
};
|
|
110
207
|
console.log(chalk_1.default.blue('Current configuration:'));
|
|
111
|
-
console.log(` Host:
|
|
112
|
-
console.log(` API Key:
|
|
208
|
+
console.log(` Host: ${config.host.padEnd(40)} ${fmt(sources.host)}`);
|
|
209
|
+
console.log(` API Key: ${maskedKey.padEnd(40)} ${fmt(sources.apiKey)}`);
|
|
210
|
+
console.log(` Workspace: ${(config.workspaceId ?? '').padEnd(40)} ${fmt(sources.workspaceId)}`);
|
|
113
211
|
}
|
|
@@ -8,7 +8,7 @@ exports.workspaceSet = workspaceSet;
|
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const api_1 = require("../utils/api");
|
|
11
|
-
const
|
|
11
|
+
const config_1 = require("../utils/config");
|
|
12
12
|
async function workspacesList() {
|
|
13
13
|
const config = (0, api_1.requireConfig)();
|
|
14
14
|
try {
|
|
@@ -53,8 +53,13 @@ async function workspacesList() {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
async function workspaceSet(workspaceId) {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (process.env.SOLIDACTIONS_WORKSPACE_ID) {
|
|
57
|
+
console.error(chalk_1.default.red('SOLIDACTIONS_WORKSPACE_ID is set in the environment; the change would not take effect. ' +
|
|
58
|
+
'Unset the env var or edit the config file directly.'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const resolved = (0, api_1.requireResolvedConfig)();
|
|
62
|
+
const config = resolved.config;
|
|
58
63
|
try {
|
|
59
64
|
const response = await axios_1.default.get(`${config.host}/api/v1/workspaces`, {
|
|
60
65
|
headers: {
|
|
@@ -62,7 +67,6 @@ async function workspaceSet(workspaceId) {
|
|
|
62
67
|
'Accept': 'application/json',
|
|
63
68
|
},
|
|
64
69
|
});
|
|
65
|
-
// Flatten grouped response
|
|
66
70
|
const grouped = response.data.workspaces || response.data.data || response.data;
|
|
67
71
|
let allWorkspaces = [];
|
|
68
72
|
if (typeof grouped === 'object' && !Array.isArray(grouped)) {
|
|
@@ -78,9 +82,10 @@ async function workspaceSet(workspaceId) {
|
|
|
78
82
|
console.error(chalk_1.default.red(`Workspace "${workspaceId}" not found. Run \`solidactions workspace list\` to list available workspaces.`));
|
|
79
83
|
process.exit(1);
|
|
80
84
|
}
|
|
81
|
-
config
|
|
82
|
-
(0,
|
|
85
|
+
const updated = { ...config, workspaceId: workspace.id };
|
|
86
|
+
(0, config_1.writeConfigFile)(resolved.activePath, updated);
|
|
83
87
|
console.log(chalk_1.default.green(`Workspace set to: ${workspace.name} (${workspace.id})`));
|
|
88
|
+
console.log(chalk_1.default.gray(`Saved to ${resolved.activePath}`));
|
|
84
89
|
}
|
|
85
90
|
catch (error) {
|
|
86
91
|
console.error(chalk_1.default.red('Failed to set workspace:'), error.response?.data?.message || error.message);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
4
8
|
const commander_1 = require("commander");
|
|
5
9
|
const deploy_1 = require("./commands/deploy");
|
|
6
10
|
const init_1 = require("./commands/init");
|
|
@@ -27,6 +31,28 @@ const workspaces_1 = require("./commands/workspaces");
|
|
|
27
31
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
28
32
|
const pkg = require('../package.json');
|
|
29
33
|
const program = new commander_1.Command();
|
|
34
|
+
if (process.env.SOLIDACTIONS_DEBUG === '1') {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
36
|
+
const { resolveConfig } = require('./utils/config');
|
|
37
|
+
const resolved = resolveConfig();
|
|
38
|
+
if (resolved) {
|
|
39
|
+
const fmt = (src) => {
|
|
40
|
+
if (src === 'env')
|
|
41
|
+
return '(from $SOLIDACTIONS_* env var)';
|
|
42
|
+
if (src === null)
|
|
43
|
+
return '(unset)';
|
|
44
|
+
return `(from ${src})`;
|
|
45
|
+
};
|
|
46
|
+
process.stderr.write('[SOLIDACTIONS_DEBUG] resolved configuration:\n');
|
|
47
|
+
process.stderr.write(` host: ${resolved.config.host} ${fmt(resolved.sources.host)}\n`);
|
|
48
|
+
process.stderr.write(` apiKey: <redacted> ${fmt(resolved.sources.apiKey)}\n`);
|
|
49
|
+
process.stderr.write(` workspaceId: ${resolved.config.workspaceId ?? ''} ${fmt(resolved.sources.workspaceId)}\n`);
|
|
50
|
+
process.stderr.write(` activePath: ${resolved.activePath}\n`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
process.stderr.write('[SOLIDACTIONS_DEBUG] no config resolvable\n');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
30
56
|
program
|
|
31
57
|
.name('solidactions')
|
|
32
58
|
.description('SolidActions CLI - Deploy and manage workflow automation')
|
|
@@ -41,14 +67,19 @@ program
|
|
|
41
67
|
.option('--dev', 'Use local development server (http://localhost:8000)')
|
|
42
68
|
.option('--host <url>', 'Custom API host URL')
|
|
43
69
|
.option('--workspace <name-or-id>', 'Set workspace by name, slug, or ID (skips interactive prompt)')
|
|
44
|
-
.
|
|
45
|
-
(
|
|
70
|
+
.option('--local', 'Save config to ./.solidactions/config.json in the current folder')
|
|
71
|
+
.option('--global', 'Save config to ~/.solidactions/config.json (default if prompted)')
|
|
72
|
+
.option('--gitignore', 'With --local, add .solidactions/ to .gitignore without prompting')
|
|
73
|
+
.action(async (apiKey, options) => {
|
|
74
|
+
await (0, init_1.init)(apiKey, options);
|
|
46
75
|
});
|
|
47
76
|
program
|
|
48
77
|
.command('logout')
|
|
49
78
|
.description('Remove saved credentials')
|
|
50
|
-
.
|
|
51
|
-
(
|
|
79
|
+
.option('--local', 'Remove only the nearest local ./.solidactions/config.json')
|
|
80
|
+
.option('--global', 'Remove only ~/.solidactions/config.json')
|
|
81
|
+
.action((options) => {
|
|
82
|
+
(0, init_1.logout)(options);
|
|
52
83
|
});
|
|
53
84
|
program
|
|
54
85
|
.command('whoami')
|
|
@@ -73,7 +104,7 @@ project
|
|
|
73
104
|
.description('Deploy a project to SolidActions')
|
|
74
105
|
.argument('<project-name>', 'Project name (will be created if it doesn\'t exist)')
|
|
75
106
|
.argument('[path]', 'Source directory to deploy (defaults to current directory)')
|
|
76
|
-
.option('-e, --env <environment>', 'Target environment (production/staging/dev)
|
|
107
|
+
.option('-e, --env <environment>', 'Target environment (production/staging/dev). Required on first deploy of a new project.')
|
|
77
108
|
.option('--create', 'Create environment project if it doesn\'t exist')
|
|
78
109
|
.option('--config-only', 'Sync YAML env declarations without building/deploying')
|
|
79
110
|
.action((projectName, path, options) => {
|
|
@@ -283,6 +314,7 @@ ai
|
|
|
283
314
|
.description('Install AI helper documentation (CLAUDE.md or AGENTS.md) for AI-assisted workflow development')
|
|
284
315
|
.option('--claude', 'Use CLAUDE.md (for Claude Code)')
|
|
285
316
|
.option('--agents', 'Use AGENTS.md (for Cursor, Windsurf, etc.)')
|
|
317
|
+
.option('--no-skills', 'Skip installing SolidActions skill files (uses legacy full CLAUDE.md injection)')
|
|
286
318
|
.action((options) => { (0, ai_init_1.aiInit)(options); });
|
|
287
319
|
ai
|
|
288
320
|
.command('examples')
|
|
@@ -291,4 +323,7 @@ ai
|
|
|
291
323
|
.option('--all', 'Install all available examples')
|
|
292
324
|
.option('--overwrite', 'Overwrite existing examples without warning')
|
|
293
325
|
.action((names, options) => { (0, ai_examples_1.aiExamples)(names, options); });
|
|
294
|
-
program.
|
|
326
|
+
program.parseAsync().catch((err) => {
|
|
327
|
+
console.error(chalk_1.default.red(err.message ?? String(err)));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
});
|
package/dist/utils/api.js
CHANGED
|
@@ -4,40 +4,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getApiHeaders = getApiHeaders;
|
|
7
|
-
exports.
|
|
7
|
+
exports.requireResolvedConfig = requireResolvedConfig;
|
|
8
8
|
exports.requireConfig = requireConfig;
|
|
9
|
+
exports.ensureWorkspaceSelected = ensureWorkspaceSelected;
|
|
9
10
|
exports.requireConfigWithWorkspace = requireConfigWithWorkspace;
|
|
10
11
|
const axios_1 = __importDefault(require("axios"));
|
|
11
12
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
13
|
const readline_1 = __importDefault(require("readline"));
|
|
13
|
-
const
|
|
14
|
-
/**
|
|
15
|
-
* Get standard API headers including X-Workspace-Id if configured.
|
|
16
|
-
*/
|
|
14
|
+
const config_1 = require("./config");
|
|
17
15
|
function getApiHeaders(config, contentType) {
|
|
18
16
|
const headers = {
|
|
19
17
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
20
18
|
'Accept': 'application/json',
|
|
21
19
|
};
|
|
22
|
-
if (contentType)
|
|
20
|
+
if (contentType)
|
|
23
21
|
headers['Content-Type'] = contentType;
|
|
24
|
-
|
|
25
|
-
if (config.workspaceId) {
|
|
22
|
+
if (config.workspaceId)
|
|
26
23
|
headers['X-Workspace-Id'] = config.workspaceId;
|
|
27
|
-
}
|
|
28
24
|
return headers;
|
|
29
25
|
}
|
|
30
26
|
/**
|
|
31
|
-
*
|
|
32
|
-
* (if only one) or prompts the user to choose.
|
|
33
|
-
*
|
|
34
|
-
* Returns the config with workspaceId set, or exits if no workspaces available.
|
|
27
|
+
* Get the full resolution (config + sources + activePath). Exits if nothing resolvable.
|
|
35
28
|
*/
|
|
29
|
+
function requireResolvedConfig() {
|
|
30
|
+
const resolved = (0, config_1.resolveConfig)();
|
|
31
|
+
if (!resolved || !resolved.config.apiKey) {
|
|
32
|
+
console.error(chalk_1.default.red('Not initialized. Run `solidactions init <api-key>` first.'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
function requireConfig() {
|
|
38
|
+
return requireResolvedConfig().config;
|
|
39
|
+
}
|
|
36
40
|
async function ensureWorkspaceSelected(config) {
|
|
37
41
|
if (config.workspaceId) {
|
|
38
42
|
return config;
|
|
39
43
|
}
|
|
40
|
-
//
|
|
44
|
+
// Re-resolve so we know whether a save would be redundant (env-provided) or meaningful (file-backed).
|
|
45
|
+
const resolved = (0, config_1.resolveConfig)();
|
|
46
|
+
const workspaceSource = resolved?.sources.workspaceId ?? null;
|
|
41
47
|
let workspaces;
|
|
42
48
|
try {
|
|
43
49
|
const response = await axios_1.default.get(`${config.host}/api/v1/workspaces`, {
|
|
@@ -46,8 +52,6 @@ async function ensureWorkspaceSelected(config) {
|
|
|
46
52
|
'Accept': 'application/json',
|
|
47
53
|
},
|
|
48
54
|
});
|
|
49
|
-
// API returns { workspaces: { "Org Name": [{ id, name, role, tenant_name, ... }] } }
|
|
50
|
-
// Flatten the grouped structure into a flat array
|
|
51
55
|
const grouped = response.data.workspaces || response.data.teams || response.data.data || response.data;
|
|
52
56
|
if (typeof grouped === 'object' && !Array.isArray(grouped)) {
|
|
53
57
|
workspaces = [];
|
|
@@ -85,7 +89,6 @@ async function ensureWorkspaceSelected(config) {
|
|
|
85
89
|
console.log(chalk_1.default.gray(`Auto-selected workspace: ${selected.name}`));
|
|
86
90
|
}
|
|
87
91
|
else {
|
|
88
|
-
// Prompt user to select
|
|
89
92
|
console.log(chalk_1.default.blue('\nSelect a workspace:\n'));
|
|
90
93
|
workspaces.forEach((ws, i) => {
|
|
91
94
|
console.log(` ${chalk_1.default.white(`${i + 1}.`)} ${ws.name} ${chalk_1.default.gray(`(${ws.org_name}, ${ws.role})`)}`);
|
|
@@ -103,26 +106,14 @@ async function ensureWorkspaceSelected(config) {
|
|
|
103
106
|
}
|
|
104
107
|
selected = workspaces[index];
|
|
105
108
|
}
|
|
106
|
-
// Save workspace selection to config
|
|
107
109
|
config.workspaceId = selected.id;
|
|
108
|
-
(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get config and ensure it's initialized. Exits if not.
|
|
114
|
-
*/
|
|
115
|
-
function requireConfig() {
|
|
116
|
-
const config = (0, init_1.getConfig)();
|
|
117
|
-
if (!config?.apiKey) {
|
|
118
|
-
console.error(chalk_1.default.red('Not initialized. Run `solidactions init <api-key>` first.'));
|
|
119
|
-
process.exit(1);
|
|
110
|
+
if (workspaceSource !== 'env') {
|
|
111
|
+
const targetPath = resolved?.activePath ?? (0, config_1.getGlobalConfigPath)();
|
|
112
|
+
(0, config_1.writeConfigFile)(targetPath, config);
|
|
120
113
|
}
|
|
114
|
+
console.log(chalk_1.default.green(`Workspace set: ${selected.name}`));
|
|
121
115
|
return config;
|
|
122
116
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Get config with workspace selected. Use this for any command that needs workspace context.
|
|
125
|
-
*/
|
|
126
117
|
async function requireConfigWithWorkspace() {
|
|
127
118
|
const config = requireConfig();
|
|
128
119
|
return ensureWorkspaceSelected(config);
|
|
@@ -0,0 +1,138 @@
|
|
|
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.getGlobalConfigPath = getGlobalConfigPath;
|
|
7
|
+
exports.getLocalConfigPath = getLocalConfigPath;
|
|
8
|
+
exports.findLocalConfigPath = findLocalConfigPath;
|
|
9
|
+
exports.readConfigFile = readConfigFile;
|
|
10
|
+
exports.writeConfigFile = writeConfigFile;
|
|
11
|
+
exports.removeConfigFile = removeConfigFile;
|
|
12
|
+
exports.resolveConfig = resolveConfig;
|
|
13
|
+
// src/utils/config.ts
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const GLOBAL_DIR = path_1.default.join(os_1.default.homedir(), '.solidactions');
|
|
18
|
+
const GLOBAL_FILE = path_1.default.join(GLOBAL_DIR, 'config.json');
|
|
19
|
+
const LOCAL_DIR_NAME = '.solidactions';
|
|
20
|
+
const LOCAL_FILE_NAME = 'config.json';
|
|
21
|
+
function getGlobalConfigPath() {
|
|
22
|
+
return GLOBAL_FILE;
|
|
23
|
+
}
|
|
24
|
+
function getLocalConfigPath(cwd = process.cwd()) {
|
|
25
|
+
return path_1.default.join(cwd, LOCAL_DIR_NAME, LOCAL_FILE_NAME);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Walk up from startDir looking for `.solidactions/config.json`.
|
|
29
|
+
* Stops at filesystem root. **Skips `$HOME` itself** so the global config
|
|
30
|
+
* at `~/.solidactions/config.json` is never matched as a local hit.
|
|
31
|
+
* Returns the absolute path of the nearest local config, or null.
|
|
32
|
+
*/
|
|
33
|
+
function findLocalConfigPath(startDir = process.cwd()) {
|
|
34
|
+
const home = os_1.default.homedir();
|
|
35
|
+
let dir = path_1.default.resolve(startDir);
|
|
36
|
+
while (true) {
|
|
37
|
+
if (dir !== home) {
|
|
38
|
+
const candidate = path_1.default.join(dir, LOCAL_DIR_NAME, LOCAL_FILE_NAME);
|
|
39
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
40
|
+
return candidate;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const parent = path_1.default.dirname(dir);
|
|
44
|
+
if (parent === dir) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read a config file. Normalizes the legacy `token` field into `apiKey`.
|
|
52
|
+
* Returns null if the file does not exist or cannot be parsed.
|
|
53
|
+
*/
|
|
54
|
+
function readConfigFile(filePath) {
|
|
55
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
|
|
60
|
+
if (raw.token && !raw.apiKey) {
|
|
61
|
+
raw.apiKey = raw.token;
|
|
62
|
+
}
|
|
63
|
+
return raw;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Atomic write: writes to `<filePath>.tmp` and renames into place.
|
|
71
|
+
* Creates parent directory with mode 0o700 if missing.
|
|
72
|
+
* File is written with mode 0o600.
|
|
73
|
+
*/
|
|
74
|
+
function writeConfigFile(filePath, config) {
|
|
75
|
+
const dir = path_1.default.dirname(filePath);
|
|
76
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
77
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
78
|
+
}
|
|
79
|
+
const tmp = `${filePath}.tmp`;
|
|
80
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
81
|
+
fs_1.default.renameSync(tmp, filePath);
|
|
82
|
+
}
|
|
83
|
+
function removeConfigFile(filePath) {
|
|
84
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
fs_1.default.unlinkSync(filePath);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function readEnvOverrides() {
|
|
91
|
+
const env = {};
|
|
92
|
+
if (process.env.SOLIDACTIONS_HOST)
|
|
93
|
+
env.host = process.env.SOLIDACTIONS_HOST;
|
|
94
|
+
if (process.env.SOLIDACTIONS_API_KEY)
|
|
95
|
+
env.apiKey = process.env.SOLIDACTIONS_API_KEY;
|
|
96
|
+
if (process.env.SOLIDACTIONS_WORKSPACE_ID)
|
|
97
|
+
env.workspaceId = process.env.SOLIDACTIONS_WORKSPACE_ID;
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve config by merging three layers field-by-field (env > local > global).
|
|
102
|
+
* Returns null only when no source contributes an apiKey AND host (i.e. nothing usable).
|
|
103
|
+
* `activePath` is the file a write-mutating command should target: nearest local if present, else global.
|
|
104
|
+
*/
|
|
105
|
+
function resolveConfig(cwd = process.cwd()) {
|
|
106
|
+
const env = readEnvOverrides();
|
|
107
|
+
const localPath = findLocalConfigPath(cwd);
|
|
108
|
+
const local = localPath ? readConfigFile(localPath) : null;
|
|
109
|
+
const global = readConfigFile(getGlobalConfigPath());
|
|
110
|
+
const pick = (key) => {
|
|
111
|
+
if (env[key] !== undefined)
|
|
112
|
+
return { value: env[key], source: 'env' };
|
|
113
|
+
if (local && local[key] !== undefined)
|
|
114
|
+
return { value: local[key], source: localPath };
|
|
115
|
+
if (global && global[key] !== undefined)
|
|
116
|
+
return { value: global[key], source: getGlobalConfigPath() };
|
|
117
|
+
return { value: undefined, source: null };
|
|
118
|
+
};
|
|
119
|
+
const host = pick('host');
|
|
120
|
+
const apiKey = pick('apiKey');
|
|
121
|
+
const workspaceId = pick('workspaceId');
|
|
122
|
+
if (!host.value && !apiKey.value) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
config: {
|
|
127
|
+
host: (host.value ?? ''),
|
|
128
|
+
apiKey: (apiKey.value ?? ''),
|
|
129
|
+
workspaceId: workspaceId.value,
|
|
130
|
+
},
|
|
131
|
+
sources: {
|
|
132
|
+
host: host.source,
|
|
133
|
+
apiKey: apiKey.source,
|
|
134
|
+
workspaceId: workspaceId.source,
|
|
135
|
+
},
|
|
136
|
+
activePath: localPath ?? getGlobalConfigPath(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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.detectSkillTargets = detectSkillTargets;
|
|
7
|
+
exports.installSkills = installSkills;
|
|
8
|
+
exports.fetchClaudeMdContent = fetchClaudeMdContent;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
12
|
+
const github_1 = require("./github");
|
|
13
|
+
const SKILL_NAMES = [
|
|
14
|
+
'solidactions-getting-started',
|
|
15
|
+
'solidactions-workflow-coding',
|
|
16
|
+
'solidactions-deploy-and-config',
|
|
17
|
+
];
|
|
18
|
+
const EXAMPLES_OWNER = 'SolidActions';
|
|
19
|
+
const EXAMPLES_REPO = 'solidactions-examples';
|
|
20
|
+
const SKILLS_PATH_PREFIX = 'skills';
|
|
21
|
+
/**
|
|
22
|
+
* Detect which AI-tool skill directories should receive skills.
|
|
23
|
+
* Returns absolute paths to the target skills/ subdirectories.
|
|
24
|
+
*
|
|
25
|
+
* v1 supports Claude Code only. Codex was resolved as user-global
|
|
26
|
+
* (~/.codex/skills) with no project-local equivalent, so it is
|
|
27
|
+
* intentionally NOT a target here.
|
|
28
|
+
*
|
|
29
|
+
* Detection rules:
|
|
30
|
+
* - If `.claude/` exists in cwd → include `.claude/skills/`
|
|
31
|
+
* - If neither exists → return empty (caller should prompt)
|
|
32
|
+
*/
|
|
33
|
+
function detectSkillTargets(cwd = process.cwd()) {
|
|
34
|
+
const targets = [];
|
|
35
|
+
if (fs_1.default.existsSync(path_1.default.join(cwd, '.claude'))) {
|
|
36
|
+
targets.push(path_1.default.join(cwd, '.claude', 'skills'));
|
|
37
|
+
}
|
|
38
|
+
return targets;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fetch all SolidActions skill files from the examples repo and write
|
|
42
|
+
* them into each target directory. Overwrites existing files (skills
|
|
43
|
+
* are versioned upstream).
|
|
44
|
+
*/
|
|
45
|
+
async function installSkills(targetDirs) {
|
|
46
|
+
const written = [];
|
|
47
|
+
for (const dir of targetDirs) {
|
|
48
|
+
fs_extra_1.default.ensureDirSync(dir);
|
|
49
|
+
}
|
|
50
|
+
for (const skillName of SKILL_NAMES) {
|
|
51
|
+
const remotePath = `${SKILLS_PATH_PREFIX}/${skillName}.md`;
|
|
52
|
+
const content = await (0, github_1.fetchRawFile)(EXAMPLES_OWNER, EXAMPLES_REPO, remotePath);
|
|
53
|
+
for (const dir of targetDirs) {
|
|
54
|
+
const filePath = path_1.default.join(dir, `${skillName}.md`);
|
|
55
|
+
fs_1.default.writeFileSync(filePath, content, 'utf8');
|
|
56
|
+
written.push(filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { written };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pick the right CLAUDE.md content variant from the examples repo
|
|
63
|
+
* based on whether skills are being installed.
|
|
64
|
+
*
|
|
65
|
+
* - skillsInstalled = true → fetches CLAUDE-skills-pointer.md (slim)
|
|
66
|
+
* - skillsInstalled = false → fetches CLAUDE.md (full, legacy)
|
|
67
|
+
*/
|
|
68
|
+
async function fetchClaudeMdContent(skillsInstalled) {
|
|
69
|
+
const file = skillsInstalled ? 'CLAUDE-skills-pointer.md' : 'CLAUDE.md';
|
|
70
|
+
return (0, github_1.fetchRawFile)(EXAMPLES_OWNER, EXAMPLES_REPO, file);
|
|
71
|
+
}
|