@solidactions/cli 1.10.3 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/dev.js +408 -92
- package/dist/commands/oauth-actions-show.js +123 -9
- package/dist/commands/skill-push.js +174 -0
- package/dist/index.js +20 -0
- package/dist/utils/mcp.js +117 -0
- package/package.json +1 -1
package/dist/commands/dev.js
CHANGED
|
@@ -1,49 +1,385 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.resolveProjectSlug = resolveProjectSlug;
|
|
40
|
+
exports.runDev = runDev;
|
|
6
41
|
exports.dev = dev;
|
|
7
42
|
const child_process_1 = require("child_process");
|
|
8
43
|
const path_1 = __importDefault(require("path"));
|
|
9
44
|
const fs_1 = __importDefault(require("fs"));
|
|
10
45
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
46
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const module_1 = require("module");
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the SA project slug for a workflow entry file and a target env.
|
|
51
|
+
*
|
|
52
|
+
* Walks up from the entry file to the project root (the directory containing
|
|
53
|
+
* `solidactions.yaml`), reads the declared `project:` name, and applies the
|
|
54
|
+
* SAME env→slug rule used by `deploy`: production keeps the bare name, every
|
|
55
|
+
* other environment appends `-<env>` (e.g. `sdk-test` → `sdk-test-dev`).
|
|
56
|
+
*
|
|
57
|
+
* Throws when the project root or the `project:` field cannot be found, so the
|
|
58
|
+
* caller surfaces a clear error instead of fetching against a bogus slug.
|
|
59
|
+
*/
|
|
60
|
+
function resolveProjectSlug(entryPath, env) {
|
|
61
|
+
const root = findSolidActionsRoot(path_1.default.resolve(entryPath));
|
|
62
|
+
if (!root) {
|
|
63
|
+
throw new Error(`could not find solidactions.yaml in any parent of ${entryPath}`);
|
|
18
64
|
}
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
65
|
+
const configPath = path_1.default.join(root, 'solidactions.yaml');
|
|
66
|
+
const config = js_yaml_1.default.load(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
67
|
+
const projectName = config?.project;
|
|
68
|
+
if (!projectName || typeof projectName !== 'string') {
|
|
69
|
+
throw new Error(`solidactions.yaml at ${configPath} is missing a top-level "project:" name`);
|
|
24
70
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
return env === 'production' ? projectName : `${projectName}-${env}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Walk up from a starting path looking for the directory that holds
|
|
75
|
+
* `solidactions.yaml` (the project root). Returns null if none is found.
|
|
76
|
+
*/
|
|
77
|
+
function findSolidActionsRoot(startPath) {
|
|
78
|
+
let dir = fs_1.default.existsSync(startPath) && fs_1.default.statSync(startPath).isDirectory()
|
|
79
|
+
? startPath
|
|
80
|
+
: path_1.default.dirname(startPath);
|
|
81
|
+
for (let i = 0; i < 10; i++) {
|
|
82
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, 'solidactions.yaml'))) {
|
|
83
|
+
return dir;
|
|
84
|
+
}
|
|
85
|
+
const parent = path_1.default.dirname(dir);
|
|
86
|
+
if (parent === dir)
|
|
87
|
+
break;
|
|
88
|
+
dir = parent;
|
|
31
89
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Run a workflow entry file in local-dev mode:
|
|
94
|
+
* 1. Fetch declared vars + connections from the platform (via api seam).
|
|
95
|
+
* 2. Apply any varsOverride, warning when a key shadows a platform var.
|
|
96
|
+
* 3. Spin up an SDK mock backend (createMockServer) for durable-primitive support.
|
|
97
|
+
* 4. import() the workflow module to populate the registry.
|
|
98
|
+
* 5. invoke() the first registered workflow descriptor.
|
|
99
|
+
* 6. Return { stdout, stderr, result }.
|
|
100
|
+
*
|
|
101
|
+
* Never calls process.exit; never lets exceptions escape (returns failed result).
|
|
102
|
+
*/
|
|
103
|
+
async function runDev(opts) {
|
|
104
|
+
const stdoutLines = [];
|
|
105
|
+
const stderrLines = [];
|
|
106
|
+
function out(msg) {
|
|
107
|
+
stdoutLines.push(msg);
|
|
108
|
+
}
|
|
109
|
+
function err(msg) {
|
|
110
|
+
stderrLines.push(msg);
|
|
111
|
+
}
|
|
112
|
+
// 1. Determine the API client (only when an env was requested). With no
|
|
113
|
+
// --env, we run fully locally: NO platform fetch, ctx.vars starts empty.
|
|
114
|
+
let apiClient;
|
|
115
|
+
if (opts.env) {
|
|
116
|
+
if (opts.api) {
|
|
117
|
+
apiClient = opts.api;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Production path: build from CLI config.
|
|
121
|
+
// Lazy-import to keep tests fast (no config resolution needed).
|
|
122
|
+
const { requireConfigWithWorkspace } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
|
|
123
|
+
const { getApiHeaders } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
|
|
124
|
+
const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
125
|
+
const config = await requireConfigWithWorkspace();
|
|
126
|
+
// Real project resolution: read the project name from the workflow
|
|
127
|
+
// file's solidactions.yaml and apply the deploy env→slug rule.
|
|
128
|
+
const projectSlug = resolveProjectSlug(opts.entry, opts.env);
|
|
129
|
+
apiClient = {
|
|
130
|
+
projectSlug,
|
|
131
|
+
async fetchVarsAndConnections(_env) {
|
|
132
|
+
const response = await axios.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?resolve_oauth=true`, { headers: getApiHeaders(config) });
|
|
133
|
+
return response.data || [];
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 2. Fetch vars from platform (only when an env + client are present).
|
|
139
|
+
let platformVars = [];
|
|
140
|
+
if (apiClient && opts.env) {
|
|
141
|
+
try {
|
|
142
|
+
platformVars = await apiClient.fetchVarsAndConnections(opts.env);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
err(`failed to fetch platform vars: ${e?.message ?? e}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 3. Build ctx.vars from platform vars. A declared mapping is only readable
|
|
149
|
+
// on ctx.vars when it is an OAuth connection (with proxy fields) OR has a
|
|
150
|
+
// non-null resolved_value. Mappings with no value in this env are DROPPED
|
|
151
|
+
// — and must NOT be counted in the summary (BUG #1).
|
|
152
|
+
const vars = {};
|
|
153
|
+
let connectionCount = 0;
|
|
154
|
+
let droppedCount = 0;
|
|
155
|
+
for (const pv of platformVars) {
|
|
156
|
+
if (pv.source_type === 'oauth_connection' && pv.proxy_url && pv.proxy_token && pv.connection_key) {
|
|
157
|
+
vars[pv.env_name] = {
|
|
158
|
+
key: pv.connection_key,
|
|
159
|
+
proxyUrl: pv.proxy_url,
|
|
160
|
+
proxyToken: pv.proxy_token,
|
|
161
|
+
};
|
|
162
|
+
connectionCount++;
|
|
163
|
+
}
|
|
164
|
+
else if (pv.resolved_value != null) {
|
|
165
|
+
vars[pv.env_name] = pv.resolved_value;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
droppedCount++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 4. Apply overrides, warning on shadows.
|
|
172
|
+
if (opts.varsOverride) {
|
|
173
|
+
for (const [key, value] of Object.entries(opts.varsOverride)) {
|
|
174
|
+
if (key in vars) {
|
|
175
|
+
err(`override shadows platform var: ${key}`);
|
|
176
|
+
}
|
|
177
|
+
vars[key] = value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 5. Print summary line. Report what is ACTUALLY readable on ctx.vars — the
|
|
181
|
+
// plain-var count is the number of plain vars actually placed in `vars`
|
|
182
|
+
// (total keys minus connection entries), never the raw mapping count.
|
|
183
|
+
if (opts.env) {
|
|
184
|
+
const plainVarCount = Object.keys(vars).length - connectionCount;
|
|
185
|
+
let summary = `Loaded ${plainVarCount} vars + ${connectionCount} connections from ${apiClient.projectSlug} / env ${opts.env}`;
|
|
186
|
+
if (droppedCount > 0) {
|
|
187
|
+
summary += ` (${droppedCount} declared ${droppedCount === 1 ? 'var' : 'vars'} had no value in this env and ${droppedCount === 1 ? 'was' : 'were'} skipped)`;
|
|
188
|
+
}
|
|
189
|
+
out(summary);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
out('Loaded 0 platform vars (no --env) — running locally');
|
|
193
|
+
}
|
|
194
|
+
// 6. Spin up SDK mock backend.
|
|
195
|
+
// Require createMockServer from the SDK's testing subpath.
|
|
196
|
+
//
|
|
197
|
+
// CRITICAL: root the require at the ENTRY FILE, not at __filename. The
|
|
198
|
+
// workflow module's own `import '@solidactions/sdk'` resolves the SDK
|
|
199
|
+
// relative to the entry file's project (e.g. examples/sdk-test/sdk), and
|
|
200
|
+
// it registers its workflow into THAT copy's registry singleton. If we
|
|
201
|
+
// resolved the SDK from the CLI's own node_modules instead (a different,
|
|
202
|
+
// npm-linked copy), we'd read an empty registry. Resolving from the entry's
|
|
203
|
+
// directory guarantees we hit the same SDK instance the workflow used.
|
|
204
|
+
const entryPath = path_1.default.resolve(opts.entry);
|
|
205
|
+
const _require = (0, module_1.createRequire)(entryPath);
|
|
206
|
+
// Resolve via the installed (or linked) @solidactions/sdk package.
|
|
207
|
+
const sdkTestingMain = _require.resolve('@solidactions/sdk/testing');
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
209
|
+
const { createMockServer } = _require(sdkTestingMain);
|
|
210
|
+
const mockServer = await createMockServer();
|
|
211
|
+
try {
|
|
212
|
+
// 7. Import the workflow entry file. Prefer the module's default export
|
|
213
|
+
// (a WorkflowDescriptor returned by defineWorkflow) so that repeated
|
|
214
|
+
// runDev() calls in the same test process don't need to clear and
|
|
215
|
+
// re-register from a cached module (cached CJS modules don't re-run
|
|
216
|
+
// their top-level code). If no default export, fall back to the registry.
|
|
217
|
+
const mod = await Promise.resolve(`${entryPath}`).then(s => __importStar(require(s)));
|
|
218
|
+
let descriptor;
|
|
219
|
+
if (mod?.default && typeof mod.default?.run === 'function') {
|
|
220
|
+
// Module exported a WorkflowDescriptor as its default — use it directly.
|
|
221
|
+
descriptor = mod.default;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Fall back: read from the SDK registry (populated by defineWorkflow side-effects).
|
|
225
|
+
const sdkMain = _require.resolve('@solidactions/sdk');
|
|
226
|
+
const registryPath = path_1.default.resolve(sdkMain, '..', 'invoke', 'registry.js');
|
|
227
|
+
const registry = _require(registryPath);
|
|
228
|
+
const all = registry.__getRegisteredWorkflows();
|
|
229
|
+
descriptor = all[0];
|
|
230
|
+
}
|
|
231
|
+
if (!descriptor) {
|
|
232
|
+
err('runDev: no workflows registered after importing the file.');
|
|
233
|
+
return {
|
|
234
|
+
stdout: stdoutLines.join('\n'),
|
|
235
|
+
stderr: stderrLines.join('\n'),
|
|
236
|
+
result: { status: 'failed', error: new Error('no workflow registered'), phase: 'run' },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// 8. Build InvokeCtx.
|
|
240
|
+
const runUuid = (0, crypto_1.randomUUID)();
|
|
241
|
+
const ctx = {
|
|
242
|
+
input: JSON.parse(opts.input || '{}'),
|
|
243
|
+
vars: Object.freeze(vars),
|
|
244
|
+
run: {
|
|
245
|
+
triggerId: 'local-dev',
|
|
246
|
+
runUuid,
|
|
247
|
+
runSecret: 'local-dev',
|
|
248
|
+
workerSessionId: (0, crypto_1.randomUUID)(),
|
|
249
|
+
},
|
|
250
|
+
app: {
|
|
251
|
+
appVersion: '0',
|
|
252
|
+
appId: 'local-dev',
|
|
253
|
+
tenantId: 'local-dev',
|
|
254
|
+
},
|
|
255
|
+
api: {
|
|
256
|
+
url: mockServer.baseUrl,
|
|
257
|
+
key: 'local-dev',
|
|
258
|
+
},
|
|
259
|
+
mode: 'local',
|
|
260
|
+
};
|
|
261
|
+
// 9. Create the run-row BEFORE invoking. invoke() itself never creates
|
|
262
|
+
// the durable run record — the production launcher (SolidActions.run
|
|
263
|
+
// → #initOneShotStatusRow) does that first, and step/sleep/recv
|
|
264
|
+
// sub-routes (`/runs/status/<id>/...`) 404 ("Workflow not found")
|
|
265
|
+
// against an absent row. A no-step workflow (the echo fixture) never
|
|
266
|
+
// hits those routes so it survives without this; any workflow with a
|
|
267
|
+
// runStep does not. Mirror #initOneShotStatusRow's CREATE shape.
|
|
268
|
+
const sdkMainForInvoke = _require.resolve('@solidactions/sdk');
|
|
269
|
+
const httpClientPath = path_1.default.resolve(sdkMainForInvoke, '..', 'http_client.js');
|
|
270
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
271
|
+
const { HttpClient } = _require(httpClientPath);
|
|
272
|
+
try {
|
|
273
|
+
const client = new HttpClient({ baseUrl: ctx.api.url, apiKey: ctx.api.key });
|
|
274
|
+
await client.post('/runs/status', {
|
|
275
|
+
workflowUUID: ctx.run.runUuid,
|
|
276
|
+
status: 'PENDING',
|
|
277
|
+
workflowName: descriptor.name ?? '',
|
|
278
|
+
workflowClassName: '',
|
|
279
|
+
workflowConfigName: '',
|
|
280
|
+
output: null,
|
|
281
|
+
error: null,
|
|
282
|
+
authenticatedUser: '',
|
|
283
|
+
assumedRole: '',
|
|
284
|
+
authenticatedRoles: [],
|
|
285
|
+
request: {},
|
|
286
|
+
executorId: String(ctx.run.triggerId),
|
|
287
|
+
applicationVersion: ctx.app.appVersion,
|
|
288
|
+
applicationID: ctx.app.appId,
|
|
289
|
+
createdAt: Date.now(),
|
|
290
|
+
priority: 0,
|
|
291
|
+
ownerXid: (0, crypto_1.randomUUID)(),
|
|
292
|
+
options: {},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
// Best-effort, symmetric with #initOneShotStatusRow's swallow.
|
|
297
|
+
err(`failed to create local run-row: ${e?.message ?? e}`);
|
|
298
|
+
}
|
|
299
|
+
// 10. Invoke via internal SDK path (invoke() is not in the public index).
|
|
300
|
+
const invokePath = path_1.default.resolve(sdkMainForInvoke, '..', 'invoke', 'invoke.js');
|
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
302
|
+
const { invoke } = _require(invokePath);
|
|
303
|
+
const result = await invoke(descriptor, ctx);
|
|
304
|
+
return {
|
|
305
|
+
stdout: stdoutLines.join('\n'),
|
|
306
|
+
stderr: stderrLines.join('\n'),
|
|
307
|
+
result,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await mockServer.stop();
|
|
38
312
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Env var set on the tsx re-exec child so it does NOT re-exec again. Without
|
|
316
|
+
* this guard the `--env` path would fork itself forever.
|
|
317
|
+
*/
|
|
318
|
+
const TSX_REEXEC_GUARD = 'SOLIDACTIONS_DEV_TSX_REEXEC';
|
|
319
|
+
/**
|
|
320
|
+
* Detect whether the current Node process already has a TypeScript loader
|
|
321
|
+
* registered (tsx / ts-node). When true, `await import('<file>.ts')` works
|
|
322
|
+
* in-process and we can run `runDev` directly without re-exec.
|
|
323
|
+
*/
|
|
324
|
+
function hasTsLoader() {
|
|
325
|
+
if (process.env[TSX_REEXEC_GUARD] === '1') {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
const execArgv = process.execArgv.join(' ');
|
|
329
|
+
return /tsx|ts-node/.test(execArgv) || Boolean(process._preload_modules);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Re-exec the CLI under `npx tsx` so that `runDev`'s `await import()` of a
|
|
333
|
+
* `.ts` workflow file resolves against tsx's TypeScript loader. The child runs
|
|
334
|
+
* the SAME command (`dev <file> [--env <env>] --input <json>`) with the re-exec
|
|
335
|
+
* guard set, so it falls straight through to {@link runDev}.
|
|
336
|
+
*
|
|
337
|
+
* tsx is resolved via `npx`; the child's cwd is the project root so its
|
|
338
|
+
* node_modules tsx is preferred. `env` is optional — when omitted the child
|
|
339
|
+
* runs the bare local path (no platform fetch, empty ctx.vars).
|
|
340
|
+
*/
|
|
341
|
+
function reexecUnderTsx(file, env, input, projectDir) {
|
|
342
|
+
// dist/src/commands/dev.js → CLI entry is dist/index.js (two dirs up).
|
|
343
|
+
const cliEntry = path_1.default.resolve(__dirname, '..', 'index.js');
|
|
344
|
+
const args = ['tsx', cliEntry, 'dev', file, '--input', input];
|
|
345
|
+
if (env) {
|
|
346
|
+
args.push('--env', env);
|
|
44
347
|
}
|
|
45
|
-
|
|
348
|
+
const result = (0, child_process_1.spawnSync)('npx', args, {
|
|
349
|
+
stdio: 'inherit',
|
|
350
|
+
cwd: projectDir,
|
|
351
|
+
env: { ...process.env, [TSX_REEXEC_GUARD]: '1' },
|
|
352
|
+
});
|
|
353
|
+
if (result.error) {
|
|
354
|
+
const code = result.error.code;
|
|
355
|
+
if (code === 'ENOENT') {
|
|
356
|
+
console.error(chalk_1.default.red('npx not found. Make sure Node.js is installed.'));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.error(chalk_1.default.red(`Failed to start tsx loader: ${result.error.message}`));
|
|
360
|
+
}
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
return result.status ?? 1;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Handler for `solidactions dev <file> [--env <env>]`: run the workflow LOCALLY
|
|
367
|
+
* through the in-process invoke() engine.
|
|
368
|
+
*
|
|
369
|
+
* With `--env`: pulls declared variable-mappings from the SA API and builds
|
|
370
|
+
* `ctx.vars` for the chosen environment. WITHOUT `--env`: NO platform fetch and
|
|
371
|
+
* `ctx.vars` starts empty `{}` — the host `process.env` is NEVER leaked into the
|
|
372
|
+
* workflow. Either way the workflow runs against the SDK mock backend via
|
|
373
|
+
* {@link runDev}. `.ts` entries are loaded by re-exec'ing under `npx tsx`.
|
|
374
|
+
*/
|
|
375
|
+
async function dev(file, options) {
|
|
376
|
+
const env = options.env;
|
|
46
377
|
const input = options.input || '{}';
|
|
378
|
+
const filePath = path_1.default.resolve(file);
|
|
379
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
380
|
+
console.error(chalk_1.default.red(`File not found: ${filePath}`));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
47
383
|
try {
|
|
48
384
|
JSON.parse(input);
|
|
49
385
|
}
|
|
@@ -51,65 +387,55 @@ async function dev(file, options) {
|
|
|
51
387
|
console.error(chalk_1.default.red('Invalid JSON input. Use -i \'{"key": "value"}\''));
|
|
52
388
|
process.exit(1);
|
|
53
389
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Spawn npx tsx with the bootstrap file
|
|
64
|
-
const child = (0, child_process_1.spawn)('npx', ['tsx', bootstrapPath], {
|
|
65
|
-
stdio: 'inherit',
|
|
66
|
-
cwd: projectDir,
|
|
67
|
-
});
|
|
68
|
-
child.on('error', (err) => {
|
|
69
|
-
cleanup(bootstrapPath);
|
|
70
|
-
if (err.code === 'ENOENT') {
|
|
71
|
-
console.error(chalk_1.default.red('npx not found. Make sure Node.js is installed.'));
|
|
390
|
+
// .ts entry under plain node has no loader — re-exec under tsx, then return.
|
|
391
|
+
if (!hasTsLoader() && /\.(ts|tsx|mts|cts)$/.test(filePath)) {
|
|
392
|
+
const projectDir = findProjectRoot(filePath) ?? process.cwd();
|
|
393
|
+
// Surface a clear slug-resolution error here (before forking) rather
|
|
394
|
+
// than letting the child fail with an opaque exit code. Only relevant
|
|
395
|
+
// when an env was requested (bare runs do no platform fetch).
|
|
396
|
+
if (env) {
|
|
397
|
+
try {
|
|
398
|
+
resolveProjectSlug(filePath, env);
|
|
72
399
|
}
|
|
73
|
-
|
|
74
|
-
console.error(chalk_1.default.red(`Failed
|
|
400
|
+
catch (err) {
|
|
401
|
+
console.error(chalk_1.default.red(`Failed: ${err.message}`));
|
|
402
|
+
process.exit(1);
|
|
75
403
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
process.on('SIGINT', () => {
|
|
84
|
-
cleanup(bootstrapPath);
|
|
85
|
-
child.kill('SIGINT');
|
|
86
|
-
});
|
|
87
|
-
process.on('SIGTERM', () => {
|
|
88
|
-
cleanup(bootstrapPath);
|
|
89
|
-
child.kill('SIGTERM');
|
|
90
|
-
});
|
|
404
|
+
}
|
|
405
|
+
process.exit(reexecUnderTsx(file, env, input, projectDir));
|
|
406
|
+
}
|
|
407
|
+
// In-process path (already under a TS loader, or a .js/.mjs entry).
|
|
408
|
+
let result;
|
|
409
|
+
try {
|
|
410
|
+
result = await runDev({ entry: filePath, input, env });
|
|
91
411
|
}
|
|
92
412
|
catch (err) {
|
|
93
|
-
cleanup(bootstrapPath);
|
|
94
413
|
console.error(chalk_1.default.red(`Failed: ${err.message}`));
|
|
95
414
|
process.exit(1);
|
|
96
415
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
process.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
416
|
+
if (result.stdout) {
|
|
417
|
+
console.log(result.stdout);
|
|
418
|
+
}
|
|
419
|
+
if (result.stderr) {
|
|
420
|
+
console.error(chalk_1.default.yellow(result.stderr));
|
|
421
|
+
}
|
|
422
|
+
const r = result.result;
|
|
423
|
+
if (r.status === 'completed') {
|
|
424
|
+
console.log(chalk_1.default.green('✓ completed'));
|
|
425
|
+
console.log(chalk_1.default.gray('Output:'), JSON.stringify(r.output));
|
|
426
|
+
process.exit(0);
|
|
427
|
+
}
|
|
428
|
+
if (r.status === 'suspended') {
|
|
429
|
+
console.log(chalk_1.default.yellow(`Workflow suspended: ${r.reason ?? 'unknown'}`));
|
|
430
|
+
process.exit(0);
|
|
431
|
+
}
|
|
432
|
+
if (r.status === 'cancelled') {
|
|
433
|
+
console.log(chalk_1.default.yellow('Workflow cancelled'));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
// failed
|
|
437
|
+
console.error(chalk_1.default.red(`✗ failed (${r.phase ?? 'run'}):`), r.error?.message ?? String(r.error));
|
|
438
|
+
process.exit(1);
|
|
113
439
|
}
|
|
114
440
|
function findProjectRoot(startPath) {
|
|
115
441
|
let dir = path_1.default.dirname(startPath);
|
|
@@ -124,13 +450,3 @@ function findProjectRoot(startPath) {
|
|
|
124
450
|
}
|
|
125
451
|
return null;
|
|
126
452
|
}
|
|
127
|
-
function cleanup(filePath) {
|
|
128
|
-
try {
|
|
129
|
-
if (fs_1.default.existsSync(filePath)) {
|
|
130
|
-
fs_1.default.unlinkSync(filePath);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// Ignore cleanup errors
|
|
135
|
-
}
|
|
136
|
-
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.oauthActionsShow = oauthActionsShow;
|
|
7
|
+
exports.buildSnippet = buildSnippet;
|
|
7
8
|
const axios_1 = __importDefault(require("axios"));
|
|
8
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
10
|
const api_1 = require("../utils/api");
|
|
@@ -18,7 +19,7 @@ async function oauthActionsShow(platform, actionId, options) {
|
|
|
18
19
|
process.stdout.write(JSON.stringify(action, null, 2) + '\n');
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
renderHumanReadable(action);
|
|
22
|
+
renderHumanReadable(action, options);
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
24
25
|
if (error.response?.status === 404) {
|
|
@@ -36,7 +37,7 @@ async function oauthActionsShow(platform, actionId, options) {
|
|
|
36
37
|
process.exit(1);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
function renderHumanReadable(action) {
|
|
40
|
+
function renderHumanReadable(action, options) {
|
|
40
41
|
const method = (action.method || 'GET').toUpperCase();
|
|
41
42
|
const ioSchema = action.io_schema || {};
|
|
42
43
|
const example = ioSchema.ioExample?.input;
|
|
@@ -73,15 +74,125 @@ function renderHumanReadable(action) {
|
|
|
73
74
|
console.log('');
|
|
74
75
|
}
|
|
75
76
|
console.log(chalk_1.default.bold('Paste-ready snippet:'));
|
|
76
|
-
|
|
77
|
+
if (options.legacyEnv) {
|
|
78
|
+
console.log(chalk_1.default.yellow(' (--legacy-env: deprecated form, will be removed in a future release)'));
|
|
79
|
+
}
|
|
80
|
+
console.log(chalk_1.default.gray(indent(buildSnippet(action, options.var, options.legacyEnv))));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Render a fetch() call against the SA proxy using the ctx.vars contract.
|
|
84
|
+
*
|
|
85
|
+
* The proxy requires:
|
|
86
|
+
* - Authorization: Bearer <proxyToken> (per-trigger scoped token from ctx.vars)
|
|
87
|
+
* - X-OAuth-Connection-Key: <key> (connection identifier from ctx.vars)
|
|
88
|
+
* - X-OAuth-Action-Id: <action_id> (the action being called)
|
|
89
|
+
*
|
|
90
|
+
* For modifier actions the body MUST include `connectionKey` alongside user params.
|
|
91
|
+
*
|
|
92
|
+
* Pass --var <NAME> to set the ctx.vars variable name (defaults to YOUR_CONNECTION).
|
|
93
|
+
* Pass --legacy-env to emit the old process.env form (deprecated, will be removed).
|
|
94
|
+
*/
|
|
95
|
+
function buildSnippet(action, varName, legacyEnv) {
|
|
96
|
+
if (legacyEnv) {
|
|
97
|
+
return buildLegacySnippet(action);
|
|
98
|
+
}
|
|
99
|
+
return buildCtxVarsSnippet(action, varName);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* New ctx.vars-based snippet (the live proxy contract).
|
|
103
|
+
*
|
|
104
|
+
* proxyUrl — the SA proxy base URL injected by the runtime at workflow start
|
|
105
|
+
* proxyToken — a per-trigger scoped Bearer token, NOT a long-lived secret
|
|
106
|
+
* key — the connection key used by the proxy to look up the OAuth credentials
|
|
107
|
+
*
|
|
108
|
+
* Headers sent to the proxy:
|
|
109
|
+
* Authorization: Bearer ${ctx.vars.<VAR>.proxyToken}
|
|
110
|
+
* X-OAuth-Connection-Key: ${ctx.vars.<VAR>.key}
|
|
111
|
+
* X-OAuth-Action-Id: '<action_id>'
|
|
112
|
+
*
|
|
113
|
+
* Do NOT include X-SA-Connection — that header is stripped by the proxy (STRIPPED_INBOUND).
|
|
114
|
+
*/
|
|
115
|
+
function buildCtxVarsSnippet(action, varName) {
|
|
116
|
+
const method = (action.method || 'GET').toUpperCase();
|
|
117
|
+
const platform = action.platform;
|
|
118
|
+
const varRef = varName || 'YOUR_CONNECTION';
|
|
119
|
+
const example = action.io_schema?.ioExample?.input;
|
|
120
|
+
const pathTemplate = (action.path || '').replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_m, name) => '${' + name.trim() + '}');
|
|
121
|
+
const queryEntries = example?.query ? Object.entries(example.query) : [];
|
|
122
|
+
const queryString = queryEntries.length
|
|
123
|
+
? '?' + queryEntries.flatMap(([k, v]) => {
|
|
124
|
+
const encodedKey = encodeURIComponent(k);
|
|
125
|
+
const items = Array.isArray(v) ? v : [v];
|
|
126
|
+
return items.map((item) => `${encodedKey}=\${encodeURIComponent(${JSON.stringify(item)})}`);
|
|
127
|
+
}).join('&')
|
|
128
|
+
: '';
|
|
129
|
+
// Build the headers block: Authorization, X-OAuth-Connection-Key, X-OAuth-Action-Id,
|
|
130
|
+
// then any non-proxy-managed example headers, then Content-Type if body is present.
|
|
131
|
+
const proxyHeaderLines = [
|
|
132
|
+
` 'Authorization': \`Bearer \${ctx.vars.${varRef}.proxyToken}\`,`,
|
|
133
|
+
` 'X-OAuth-Connection-Key': ctx.vars.${varRef}.key,`,
|
|
134
|
+
` 'X-OAuth-Action-Id': ${JSON.stringify(action.action_id)},`,
|
|
135
|
+
];
|
|
136
|
+
const exampleHeaders = example?.headers || {};
|
|
137
|
+
for (const [k, v] of Object.entries(exampleHeaders)) {
|
|
138
|
+
if (isProxyManagedHeader(k))
|
|
139
|
+
continue;
|
|
140
|
+
proxyHeaderLines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)},`);
|
|
141
|
+
}
|
|
142
|
+
if (example?.body !== undefined && example.body !== null) {
|
|
143
|
+
const hasContentType = Object.keys(exampleHeaders).some(k => k.toLowerCase() === 'content-type');
|
|
144
|
+
if (!hasContentType) {
|
|
145
|
+
proxyHeaderLines.push(` 'Content-Type': 'application/json',`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const headersBlock = proxyHeaderLines.join('\n');
|
|
149
|
+
const lines = [];
|
|
150
|
+
lines.push('const res = await fetch(');
|
|
151
|
+
lines.push(` \`\${ctx.vars.${varRef}.proxyUrl}/${platform}${pathTemplate}${queryString}\`,`);
|
|
152
|
+
lines.push(' {');
|
|
153
|
+
lines.push(` method: '${method}',`);
|
|
154
|
+
lines.push(' headers: {');
|
|
155
|
+
lines.push(headersBlock);
|
|
156
|
+
lines.push(' },');
|
|
157
|
+
if (example?.body !== undefined && example.body !== null) {
|
|
158
|
+
// Modifier actions: body already contains connectionKey from the API example.
|
|
159
|
+
// In the new contract the proxy uses X-OAuth-Connection-Key for routing, but
|
|
160
|
+
// modifier actions additionally require connectionKey IN the body so the
|
|
161
|
+
// upstream provider receives it. Substitute with a live ctx.vars reference.
|
|
162
|
+
const bodyObj = example.body;
|
|
163
|
+
const isModifier = typeof bodyObj === 'object' && bodyObj !== null && 'connectionKey' in bodyObj;
|
|
164
|
+
let bodyExpr;
|
|
165
|
+
if (isModifier) {
|
|
166
|
+
// Build the body object manually so we can emit `ctx.vars.<VAR>.key`
|
|
167
|
+
// (an unquoted JS expression) as the connectionKey value rather than
|
|
168
|
+
// a string literal.
|
|
169
|
+
const otherFields = Object.entries(bodyObj)
|
|
170
|
+
.filter(([k]) => k !== 'connectionKey')
|
|
171
|
+
.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`)
|
|
172
|
+
.join(',\n');
|
|
173
|
+
const otherPart = otherFields ? `,\n${otherFields}` : '';
|
|
174
|
+
bodyExpr = `{\n connectionKey: ctx.vars.${varRef}.key${otherPart}\n}`;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
bodyExpr = JSON.stringify(bodyObj, null, 2);
|
|
178
|
+
}
|
|
179
|
+
const bodyLines = bodyExpr
|
|
180
|
+
.split('\n')
|
|
181
|
+
.map((l, i) => (i === 0 ? l : ' ' + l))
|
|
182
|
+
.join('\n');
|
|
183
|
+
lines.push(` body: JSON.stringify(${bodyLines}),`);
|
|
184
|
+
}
|
|
185
|
+
lines.push(' }');
|
|
186
|
+
lines.push(');');
|
|
187
|
+
lines.push('const data = await res.json();');
|
|
188
|
+
return lines.join('\n');
|
|
77
189
|
}
|
|
78
190
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* known-good shape rather than inferring it from JSON Schema).
|
|
191
|
+
* Legacy process.env-based snippet (deprecated).
|
|
192
|
+
* Emitted only when --legacy-env is passed.
|
|
193
|
+
* This form no longer matches the live proxy contract — use the ctx.vars form.
|
|
83
194
|
*/
|
|
84
|
-
function
|
|
195
|
+
function buildLegacySnippet(action) {
|
|
85
196
|
const method = (action.method || 'GET').toUpperCase();
|
|
86
197
|
const platform = action.platform;
|
|
87
198
|
const platformEnv = platform.toUpperCase().replace(/-/g, '_');
|
|
@@ -153,5 +264,8 @@ function isProxyManagedHeader(name) {
|
|
|
153
264
|
const lower = name.toLowerCase();
|
|
154
265
|
return lower === 'authorization'
|
|
155
266
|
|| lower.startsWith('x-pica-')
|
|
156
|
-
|| lower.startsWith('x-one-')
|
|
267
|
+
|| lower.startsWith('x-one-')
|
|
268
|
+
|| lower === 'x-sa-connection'
|
|
269
|
+
|| lower === 'x-oauth-connection-key'
|
|
270
|
+
|| lower === 'x-oauth-action-id';
|
|
157
271
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* solidactions skill push <dir>
|
|
4
|
+
*
|
|
5
|
+
* Pushes a local skill folder into the crews library via the crews MCP server.
|
|
6
|
+
* Idempotent upsert: create, or update on name collision.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseSkillFile = parseSkillFile;
|
|
13
|
+
exports.readTopLevelReferences = readTopLevelReferences;
|
|
14
|
+
exports.skillPushWithConfig = skillPushWithConfig;
|
|
15
|
+
exports.skillPush = skillPush;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
20
|
+
const api_1 = require("../utils/api");
|
|
21
|
+
const mcp_1 = require("../utils/mcp");
|
|
22
|
+
/**
|
|
23
|
+
* Parse a SKILL.md file that begins with a YAML frontmatter block.
|
|
24
|
+
*
|
|
25
|
+
* Returns { name, description, properties, body } where:
|
|
26
|
+
* - name and description come from the frontmatter
|
|
27
|
+
* - properties is the rest of the frontmatter (excluding name and description)
|
|
28
|
+
* - body is everything after the closing --- delimiter
|
|
29
|
+
*
|
|
30
|
+
* Throws with a descriptive message if the file is malformed or missing
|
|
31
|
+
* required fields.
|
|
32
|
+
*/
|
|
33
|
+
function parseSkillFile(content) {
|
|
34
|
+
if (!content.startsWith('---')) {
|
|
35
|
+
throw new Error('SKILL.md must begin with a YAML frontmatter block (---).');
|
|
36
|
+
}
|
|
37
|
+
// Find the closing --- delimiter (must be on its own line after the opening ---)
|
|
38
|
+
const afterOpen = content.slice(3);
|
|
39
|
+
const closeIdx = afterOpen.indexOf('\n---');
|
|
40
|
+
if (closeIdx === -1) {
|
|
41
|
+
throw new Error('SKILL.md frontmatter is not closed (missing closing ---).');
|
|
42
|
+
}
|
|
43
|
+
const yamlBlock = afterOpen.slice(0, closeIdx);
|
|
44
|
+
// Body is everything after the closing ---\n
|
|
45
|
+
const body = afterOpen.slice(closeIdx + 4).replace(/^\n/, '');
|
|
46
|
+
let fm;
|
|
47
|
+
try {
|
|
48
|
+
fm = js_yaml_1.default.load(yamlBlock);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
throw new Error(`Failed to parse SKILL.md frontmatter YAML: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
if (!fm || typeof fm !== 'object') {
|
|
54
|
+
throw new Error('SKILL.md frontmatter is empty or not a YAML object.');
|
|
55
|
+
}
|
|
56
|
+
if (!fm.name || typeof fm.name !== 'string') {
|
|
57
|
+
throw new Error('SKILL.md frontmatter must contain a "name" field (string).');
|
|
58
|
+
}
|
|
59
|
+
if (!fm.description || typeof fm.description !== 'string') {
|
|
60
|
+
throw new Error('SKILL.md frontmatter must contain a "description" field (string).');
|
|
61
|
+
}
|
|
62
|
+
const { name, description, ...rest } = fm;
|
|
63
|
+
// Remove 'type' if present — the server sets it
|
|
64
|
+
const { type: _type, ...properties } = rest;
|
|
65
|
+
return { name, description, properties, body };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read top-level (non-recursive) files in dir, excluding SKILL.md.
|
|
69
|
+
* Returns a map of filename → utf8 contents.
|
|
70
|
+
*/
|
|
71
|
+
function readTopLevelReferences(dir) {
|
|
72
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
73
|
+
const references = {};
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry.isFile())
|
|
76
|
+
continue;
|
|
77
|
+
if (entry.name === 'SKILL.md')
|
|
78
|
+
continue;
|
|
79
|
+
const filePath = path_1.default.join(dir, entry.name);
|
|
80
|
+
references[entry.name] = fs_1.default.readFileSync(filePath, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
return references;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Core implementation — accepts an injected config so tests can point at a
|
|
86
|
+
* stub server without touching the filesystem config.
|
|
87
|
+
*/
|
|
88
|
+
async function skillPushWithConfig(dir, options, config) {
|
|
89
|
+
const absDir = path_1.default.resolve(dir);
|
|
90
|
+
// 1. Check directory exists
|
|
91
|
+
if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
|
|
92
|
+
process.stderr.write(chalk_1.default.red(`error: "${dir}" is not a directory.\n`));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// 2. Read SKILL.md
|
|
96
|
+
const skillMdPath = path_1.default.join(absDir, 'SKILL.md');
|
|
97
|
+
if (!fs_1.default.existsSync(skillMdPath)) {
|
|
98
|
+
process.stderr.write(chalk_1.default.red(`error: "${skillMdPath}" not found. The skill directory must contain a SKILL.md file.\n`));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const skillMdContent = fs_1.default.readFileSync(skillMdPath, 'utf8');
|
|
102
|
+
// 3. Parse frontmatter + body
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = parseSkillFile(skillMdContent);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const { name, description, properties, body } = parsed;
|
|
112
|
+
// 4. Read sibling reference files
|
|
113
|
+
const references = readTopLevelReferences(absDir);
|
|
114
|
+
// 5. Compose an idempotent upsert: try create; on name_collision, edit.
|
|
115
|
+
const isRole = !!options.role;
|
|
116
|
+
const tool = isRole ? 'roles' : 'skills';
|
|
117
|
+
const createArgs = isRole
|
|
118
|
+
? { action: 'create_skill', role: options.role, name, description, body, properties, references }
|
|
119
|
+
: { action: 'create', name, description, body, properties, references };
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = await (0, mcp_1.callCrewsTool)(config, tool, createArgs);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
let didUpdate = false;
|
|
129
|
+
// 6. On name collision, switch to the edit path (the upsert). properties → properties_patch.
|
|
130
|
+
if (!result.ok && result.data?.code === 'name_collision') {
|
|
131
|
+
const editArgs = isRole
|
|
132
|
+
? { action: 'edit_skill', role: options.role, name, description, body, properties_patch: properties, references }
|
|
133
|
+
: { action: 'edit', identifier: name, description, body, properties_patch: properties, references };
|
|
134
|
+
try {
|
|
135
|
+
result = await (0, mcp_1.callCrewsTool)(config, tool, editArgs);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
didUpdate = true;
|
|
142
|
+
}
|
|
143
|
+
// 7. Handle result.
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
const errData = result.data;
|
|
146
|
+
const errCode = errData?.code ?? 'unknown_error';
|
|
147
|
+
const errMsg = errData?.message ?? 'MCP returned an error with no message';
|
|
148
|
+
process.stderr.write(chalk_1.default.red(`${errCode}: ${errMsg}\n`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
if (options.json) {
|
|
152
|
+
console.log(JSON.stringify(result.data));
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
const data = result.data;
|
|
156
|
+
if (didUpdate) {
|
|
157
|
+
// edit shape: {version_id, body_blob_sha}
|
|
158
|
+
console.log(chalk_1.default.green(`updated skill '${name}' (version ${data.version_id ?? '?'})`));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// create shape: {skill_doc_id, folder_id, reference_doc_ids}
|
|
162
|
+
const skillDocId = data.skill_doc_id ?? data.doc_id ?? data.id ?? '?';
|
|
163
|
+
const refCount = Object.keys(data.reference_doc_ids ?? {}).length;
|
|
164
|
+
console.log(chalk_1.default.green(`created skill '${name}' (doc ${skillDocId}, ${refCount} refs)`));
|
|
165
|
+
}
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Entry point called from index.ts.
|
|
170
|
+
*/
|
|
171
|
+
async function skillPush(dir, options) {
|
|
172
|
+
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
173
|
+
await skillPushWithConfig(dir, options, config);
|
|
174
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ const oauth_actions_search_1 = require("./commands/oauth-actions-search");
|
|
|
32
32
|
const oauth_actions_list_1 = require("./commands/oauth-actions-list");
|
|
33
33
|
const oauth_actions_show_1 = require("./commands/oauth-actions-show");
|
|
34
34
|
const workspaces_1 = require("./commands/workspaces");
|
|
35
|
+
const skill_push_1 = require("./commands/skill-push");
|
|
35
36
|
const config_1 = require("./utils/config");
|
|
36
37
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
37
38
|
const pkg = require('../package.json');
|
|
@@ -123,7 +124,11 @@ program
|
|
|
123
124
|
.description('Run a workflow locally using an in-memory mock server (no deploy needed)')
|
|
124
125
|
.argument('<file>', 'Workflow file to run (e.g., src/simple-steps.ts)')
|
|
125
126
|
.option('-i, --input <json>', 'JSON input for the workflow', '{}')
|
|
127
|
+
.option('-e, --env <env>', 'Pull platform variables for this environment (e.g. dev, staging, production)')
|
|
126
128
|
.action((file, options) => {
|
|
129
|
+
// Unified in-process invoke path. With --env: fetch declared vars, build
|
|
130
|
+
// ctx.vars, invoke locally. Without --env: NO platform fetch, ctx.vars is
|
|
131
|
+
// empty {} (the host process.env is never leaked into the workflow).
|
|
127
132
|
(0, dev_1.dev)(file, options);
|
|
128
133
|
});
|
|
129
134
|
// =============================================================================
|
|
@@ -365,6 +370,8 @@ oauthActionsCmd
|
|
|
365
370
|
.command('show <platform> <action-id>')
|
|
366
371
|
.description('Show full schema, example body, and a paste-ready fetch snippet for one action')
|
|
367
372
|
.option('--json', 'Emit raw JSON for AI/script consumption')
|
|
373
|
+
.option('--var <NAME>', 'ctx.vars variable name for the connection (default: YOUR_CONNECTION)')
|
|
374
|
+
.option('--legacy-env', 'Emit the deprecated process.env-based snippet (will be removed in a future release)')
|
|
368
375
|
.action((platform, actionId, options) => {
|
|
369
376
|
(0, oauth_actions_show_1.oauthActionsShow)(platform, actionId, options);
|
|
370
377
|
});
|
|
@@ -385,6 +392,19 @@ ai
|
|
|
385
392
|
.option('--all', 'Install all available examples')
|
|
386
393
|
.option('--overwrite', 'Overwrite existing examples without warning')
|
|
387
394
|
.action((names, options) => { (0, ai_examples_1.aiExamples)(names, options); });
|
|
395
|
+
// =============================================================================
|
|
396
|
+
// skill <subcommand> (SOP surface — flat top-level noun per cli#34)
|
|
397
|
+
// =============================================================================
|
|
398
|
+
const skill = program.command('skill').description('Manage agent skills (crews SOP surface)');
|
|
399
|
+
skill
|
|
400
|
+
.command('push')
|
|
401
|
+
.description('Push a local skill folder into the library (create, or update if it already exists)')
|
|
402
|
+
.argument('<dir>', 'Path to the skill directory (must contain SKILL.md)')
|
|
403
|
+
.option('--role <name>', 'Scope the skill to a role instead of the shared library')
|
|
404
|
+
.option('--json', 'Output result as JSON')
|
|
405
|
+
.action(async (dir, options) => {
|
|
406
|
+
await (0, skill_push_1.skillPush)(dir, options);
|
|
407
|
+
});
|
|
388
408
|
program.parseAsync().catch((err) => {
|
|
389
409
|
console.error(chalk_1.default.red(err.message ?? String(err)));
|
|
390
410
|
process.exit(1);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal MCP client for the SolidActions crews MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Sends a single stateless JSON-RPC tools/call POST. No initialize handshake
|
|
6
|
+
* required — the streamable-HTTP transport at /mcp/crews accepts single
|
|
7
|
+
* stateless POSTs (confirmed live).
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.callCrewsTool = callCrewsTool;
|
|
44
|
+
const http = __importStar(require("http"));
|
|
45
|
+
const https = __importStar(require("https"));
|
|
46
|
+
const url_1 = require("url");
|
|
47
|
+
const api_1 = require("./api");
|
|
48
|
+
/**
|
|
49
|
+
* Call a single MCP tool on /mcp/crews.
|
|
50
|
+
*
|
|
51
|
+
* Returns { ok: true, data: <success shape> } or { ok: false, data: { code, message } }.
|
|
52
|
+
*/
|
|
53
|
+
async function callCrewsTool(config, toolName, args) {
|
|
54
|
+
const baseHeaders = (0, api_1.getApiHeaders)(config, 'application/json');
|
|
55
|
+
// Override Accept to include text/event-stream for streamable-HTTP transport
|
|
56
|
+
const headers = {
|
|
57
|
+
...baseHeaders,
|
|
58
|
+
'Accept': 'application/json, text/event-stream',
|
|
59
|
+
};
|
|
60
|
+
const body = JSON.stringify({
|
|
61
|
+
jsonrpc: '2.0',
|
|
62
|
+
id: 1,
|
|
63
|
+
method: 'tools/call',
|
|
64
|
+
params: {
|
|
65
|
+
name: toolName,
|
|
66
|
+
arguments: args,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const parsed = new url_1.URL(`${config.host}/mcp/crews`);
|
|
70
|
+
const isHttps = parsed.protocol === 'https:';
|
|
71
|
+
const transport = isHttps ? https : http;
|
|
72
|
+
const responseData = await new Promise((resolve, reject) => {
|
|
73
|
+
const options = {
|
|
74
|
+
hostname: parsed.hostname,
|
|
75
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
76
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
...headers,
|
|
80
|
+
'Content-Length': Buffer.byteLength(body),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const req = transport.request(options, (res) => {
|
|
84
|
+
let raw = '';
|
|
85
|
+
res.on('data', (chunk) => { raw += chunk; });
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
88
|
+
reject(new Error(`MCP request failed with HTTP ${res.statusCode}: ${raw}`));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
resolve(raw);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
req.on('error', reject);
|
|
96
|
+
req.write(body);
|
|
97
|
+
req.end();
|
|
98
|
+
});
|
|
99
|
+
let parsed2;
|
|
100
|
+
try {
|
|
101
|
+
parsed2 = JSON.parse(responseData);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new Error(`MCP server returned non-JSON response: ${responseData}`);
|
|
105
|
+
}
|
|
106
|
+
const result = parsed2?.result;
|
|
107
|
+
const isError = result?.isError === true;
|
|
108
|
+
const textContent = result?.content?.[0]?.text ?? '{}';
|
|
109
|
+
let toolData;
|
|
110
|
+
try {
|
|
111
|
+
toolData = JSON.parse(textContent);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error(`MCP tool result content is not valid JSON: ${textContent}`);
|
|
115
|
+
}
|
|
116
|
+
return { ok: !isError, data: toolData };
|
|
117
|
+
}
|