@solidactions/cli 1.10.3 → 1.11.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/index.js +6 -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
|
}
|
package/dist/index.js
CHANGED
|
@@ -123,7 +123,11 @@ program
|
|
|
123
123
|
.description('Run a workflow locally using an in-memory mock server (no deploy needed)')
|
|
124
124
|
.argument('<file>', 'Workflow file to run (e.g., src/simple-steps.ts)')
|
|
125
125
|
.option('-i, --input <json>', 'JSON input for the workflow', '{}')
|
|
126
|
+
.option('-e, --env <env>', 'Pull platform variables for this environment (e.g. dev, staging, production)')
|
|
126
127
|
.action((file, options) => {
|
|
128
|
+
// Unified in-process invoke path. With --env: fetch declared vars, build
|
|
129
|
+
// ctx.vars, invoke locally. Without --env: NO platform fetch, ctx.vars is
|
|
130
|
+
// empty {} (the host process.env is never leaked into the workflow).
|
|
127
131
|
(0, dev_1.dev)(file, options);
|
|
128
132
|
});
|
|
129
133
|
// =============================================================================
|
|
@@ -365,6 +369,8 @@ oauthActionsCmd
|
|
|
365
369
|
.command('show <platform> <action-id>')
|
|
366
370
|
.description('Show full schema, example body, and a paste-ready fetch snippet for one action')
|
|
367
371
|
.option('--json', 'Emit raw JSON for AI/script consumption')
|
|
372
|
+
.option('--var <NAME>', 'ctx.vars variable name for the connection (default: YOUR_CONNECTION)')
|
|
373
|
+
.option('--legacy-env', 'Emit the deprecated process.env-based snippet (will be removed in a future release)')
|
|
368
374
|
.action((platform, actionId, options) => {
|
|
369
375
|
(0, oauth_actions_show_1.oauthActionsShow)(platform, actionId, options);
|
|
370
376
|
});
|