@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.
@@ -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 dotenv_1 = __importDefault(require("dotenv"));
12
- async function dev(file, options) {
13
- // Resolve the workflow file path
14
- const filePath = path_1.default.resolve(file);
15
- if (!fs_1.default.existsSync(filePath)) {
16
- console.error(chalk_1.default.red(`File not found: ${filePath}`));
17
- process.exit(1);
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
- // Find the project root (directory containing package.json)
20
- const projectDir = findProjectRoot(filePath);
21
- if (!projectDir) {
22
- console.error(chalk_1.default.red('Could not find package.json in parent directories.'));
23
- process.exit(1);
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
- // Check that @solidactions/sdk is installed
26
- const sdkPath = path_1.default.join(projectDir, 'node_modules', '@solidactions', 'sdk');
27
- if (!fs_1.default.existsSync(sdkPath)) {
28
- console.error(chalk_1.default.red('@solidactions/sdk is not installed in this project.'));
29
- console.log(chalk_1.default.gray('Run: npm install @solidactions/sdk'));
30
- process.exit(1);
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
- // Check that the testing export exists
33
- const testingPath = path_1.default.join(sdkPath, 'dist', 'src', 'testing');
34
- if (!fs_1.default.existsSync(testingPath)) {
35
- console.error(chalk_1.default.red('@solidactions/sdk version does not include testing utilities.'));
36
- console.log(chalk_1.default.gray('Update to the latest SDK version: npm install @solidactions/sdk@latest'));
37
- process.exit(1);
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
- // Load .env file from project root
40
- const envPath = path_1.default.join(projectDir, '.env');
41
- if (fs_1.default.existsSync(envPath)) {
42
- dotenv_1.default.config({ path: envPath });
43
- console.log(chalk_1.default.gray(`Loaded .env from ${projectDir}`));
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
- // Validate input JSON if provided
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
- // Write temp bootstrap file
55
- const bootstrapPath = path_1.default.join(projectDir, '.solidactions-dev-bootstrap.mjs');
56
- const bootstrapContent = generateBootstrap(filePath, input);
57
- try {
58
- fs_1.default.writeFileSync(bootstrapPath, bootstrapContent);
59
- console.log(chalk_1.default.blue('SolidActions local dev mode'));
60
- console.log(chalk_1.default.gray(`File: ${path_1.default.relative(projectDir, filePath)}`));
61
- console.log(chalk_1.default.gray(`Input: ${input}`));
62
- console.log(chalk_1.default.gray('---'));
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
- else {
74
- console.error(chalk_1.default.red(`Failed to start: ${err.message}`));
400
+ catch (err) {
401
+ console.error(chalk_1.default.red(`Failed: ${err.message}`));
402
+ process.exit(1);
75
403
  }
76
- process.exit(1);
77
- });
78
- child.on('exit', (code) => {
79
- cleanup(bootstrapPath);
80
- process.exit(code ?? 1);
81
- });
82
- // Also clean up on signals
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
- function generateBootstrap(workflowFile, input) {
99
- // Use forward slashes for the import path (works cross-platform in ESM)
100
- const importPath = workflowFile.replace(/\\/g, '/');
101
- return `// Auto-generated by solidactions dev - do not commit
102
- import { createMockServer } from '@solidactions/sdk/testing';
103
-
104
- const server = await createMockServer();
105
-
106
- process.env.SOLIDACTIONS_API_URL = server.baseUrl;
107
- process.env.SOLIDACTIONS_API_KEY = 'local-dev';
108
- process.env.WORKFLOW_INPUT = ${JSON.stringify(input)};
109
-
110
- // Import the workflow file - this triggers SolidActions.run() at module level
111
- await import(${JSON.stringify(importPath)});
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
- console.log(chalk_1.default.gray(indent(buildSnippet(action))));
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
- * Render a fetch() call against the SA proxy with `{{name}}` path placeholders
80
- * rewritten to `${name}` template-literal slots, query parameters appended,
81
- * and the example body inlined verbatim (so the AI substitutes values into the
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 buildSnippet(action) {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.10.3",
3
+ "version": "1.11.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {