@solidactions/cli 1.20.0 → 1.22.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.
@@ -3,9 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SKILLS_TIP_LINES = void 0;
7
+ exports.cacheBusterEntry = cacheBusterEntry;
8
+ exports.shouldPrintWorkspaceMismatch = shouldPrintWorkspaceMismatch;
6
9
  exports.deploy = deploy;
10
+ exports.shouldPrintWebhookSecretNotice = shouldPrintWebhookSecretNotice;
7
11
  const fs_1 = __importDefault(require("fs"));
8
12
  const path_1 = __importDefault(require("path"));
13
+ const crypto_1 = require("crypto");
9
14
  const archiver_1 = __importDefault(require("archiver"));
10
15
  const axios_1 = __importDefault(require("axios"));
11
16
  const form_data_1 = __importDefault(require("form-data"));
@@ -15,6 +20,13 @@ const env_1 = require("../utils/env");
15
20
  const api_1 = require("../utils/api");
16
21
  const deploy_ignore_1 = require("../utils/deploy-ignore");
17
22
  const slug_1 = require("../utils/slug");
23
+ const skills_1 = require("../utils/skills");
24
+ /** Printed when a deploy target has no SolidActions skill files installed. */
25
+ exports.SKILLS_TIP_LINES = [
26
+ 'Tip: no SolidActions skill files found (.claude/skills/solidactions-*). Your AI assistant',
27
+ 'works much better with them — run `solidactions ai init --claude` (or --agents) in this',
28
+ 'directory to add them.',
29
+ ];
18
30
  /**
19
31
  * Validate project structure before deployment.
20
32
  * Checks for required files and SDK dependency.
@@ -87,6 +99,21 @@ function validateProject(sourceDir) {
87
99
  }
88
100
  return { valid: errors.length === 0, errors, warnings };
89
101
  }
102
+ /**
103
+ * When `noCache` is true, returns an archive entry with a unique name and
104
+ * random content that busts the build-layer content hash (Blaxel/Daytona S3
105
+ * MD5 and BuildKit COPY . layer). Returns null when noCache is false/undefined.
106
+ */
107
+ function cacheBusterEntry(noCache) {
108
+ if (!noCache) {
109
+ return null;
110
+ }
111
+ const uuid = (0, crypto_1.randomUUID)();
112
+ return {
113
+ name: `tenantcode/sa-nocache-${uuid}`,
114
+ content: `force-rebuild ${uuid} ${Date.now()}`,
115
+ };
116
+ }
90
117
  /**
91
118
  * Push YAML env declarations to the project.
92
119
  * This registers all YAML-declared vars and their mappings.
@@ -113,6 +140,15 @@ async function pushYamlDeclarations(config, projectSlug, yamlConfig) {
113
140
  console.error(chalk_1.default.yellow('Warning: Failed to sync YAML declarations:'), error.response?.data?.message || error.message);
114
141
  }
115
142
  }
143
+ /**
144
+ * Returns true only when a 404 on the environment-specific project lookup
145
+ * will cause the deploy command to abort (non-production environment and
146
+ * --create was not passed). On these paths the workspace-mismatch warning
147
+ * should print. On all success/create paths it must be suppressed.
148
+ */
149
+ function shouldPrintWorkspaceMismatch(environment, willCreate) {
150
+ return environment !== 'production' && !willCreate;
151
+ }
116
152
  async function deploy(projectName, sourcePath, options = {}) {
117
153
  const config = await (0, api_1.requireConfigWithWorkspace)();
118
154
  let workspaceMismatchPrinted = false;
@@ -134,6 +170,13 @@ async function deploy(projectName, sourcePath, options = {}) {
134
170
  console.error(chalk_1.default.red(`Source directory not found: ${sourceDir}`));
135
171
  process.exit(1);
136
172
  }
173
+ // Non-blocking: AI assistants work measurably better with the skill files
174
+ // (field report: a 600-line skill file cracked the env-scope bug).
175
+ if (!(0, skills_1.hasSolidActionsSkills)(sourceDir)) {
176
+ for (const line of exports.SKILLS_TIP_LINES) {
177
+ console.log(chalk_1.default.yellow(line));
178
+ }
179
+ }
137
180
  // -------------------------------------------------------------------------
138
181
  // First-deploy check — must happen BEFORE we apply any env default so that
139
182
  // first-time deploys without -e get a helpful error prompting a deliberate
@@ -152,11 +195,8 @@ async function deploy(projectName, sourcePath, options = {}) {
152
195
  catch (error) {
153
196
  if (error.response?.status === 404) {
154
197
  productionExists = false;
155
- // App PR #128 returns "Project '<slug>' not found in your active workspace '<ws>'." on 404.
156
- // The axios interceptor (utils/api.ts) already appended the "Did you mean to switch workspaces?"
157
- // hint. Surface that augmented message so the user sees the hint before falling through to
158
- // the first-deploy flow.
159
- printWorkspaceMismatchOnce(error);
198
+ // Project doesn't exist yet this is the normal first-deploy path.
199
+ // Do NOT print a warning here; the deploy proceeds to create/deploy successfully.
160
200
  }
161
201
  else {
162
202
  // 5xx, network error, auth failure, etc. — fail conservatively rather
@@ -231,12 +271,11 @@ async function deploy(projectName, sourcePath, options = {}) {
231
271
  }
232
272
  catch (error) {
233
273
  if (error.response?.status === 404) {
234
- // App PR #128 returns "Project '<slug>' not found in your active workspace '<ws>'." on 404.
235
- // Surface the augmented message (axios interceptor already appended the hint) before
236
- // falling through to the --create / first-deploy flow.
237
- printWorkspaceMismatchOnce(error);
238
- // For non-production environments, require --create or give a clear hint
239
- if (environment !== 'production' && !options.create) {
274
+ // For non-production environments, require --create or give a clear hint.
275
+ // Only print the workspace-mismatch warning on this abort path not on
276
+ // the success/create path (shouldPrintWorkspaceMismatch guards this).
277
+ if (shouldPrintWorkspaceMismatch(environment, options.create ?? false)) {
278
+ printWorkspaceMismatchOnce(error);
240
279
  console.error(chalk_1.default.red(`\nProject "${projectName}" doesn't have a ${environment} environment.\n`));
241
280
  console.error(` If production is the intended target:`);
242
281
  console.error(` solidactions project deploy ${projectName} <path> -e production`);
@@ -365,6 +404,12 @@ async function deploy(projectName, sourcePath, options = {}) {
365
404
  if (yamlConfig) {
366
405
  await pushYamlDeclarations(config, projectSlug, yamlConfig);
367
406
  }
407
+ if (yamlConfig && shouldPrintWebhookSecretNotice(yamlConfig.workflows ?? [])) {
408
+ const envFlag = environment !== 'dev' ? ` -e ${environment}` : '';
409
+ console.log('');
410
+ console.log(chalk_1.default.blue(`ℹ Webhook secret: run \`solidactions webhook secret ${projectName}${envFlag}\` to retrieve the generated secret.`));
411
+ console.log(chalk_1.default.gray(` Set the same value in your sender (e.g. Telegram setWebhook secret_token).`));
412
+ }
368
413
  fs_1.default.unlinkSync(archivePath);
369
414
  process.exit(0);
370
415
  }
@@ -429,5 +474,27 @@ async function deploy(projectName, sourcePath, options = {}) {
429
474
  'RUN npm run build',
430
475
  ].join('\n') + '\n';
431
476
  archive.append(universalDockerfile, { name: 'Dockerfile' });
477
+ // --no-cache / --force-rebuild: inject a unique random-content file so all
478
+ // cache layers (Blaxel content hash, S3 context.tar MD5, BuildKit COPY .)
479
+ // see a new directory fingerprint and are forced to rebuild from scratch.
480
+ const buster = cacheBusterEntry(options.noCache ?? false);
481
+ if (buster) {
482
+ console.log(chalk_1.default.yellow('🔄 --no-cache: injecting cache-buster, forcing a fresh build'));
483
+ archive.append(buster.content, { name: buster.name });
484
+ }
432
485
  await archive.finalize();
433
486
  }
487
+ /**
488
+ * Returns true if any workflow in the project has a webhook trigger that
489
+ * uses HMAC or header authentication (i.e. requires a shared secret).
490
+ * Used to gate the post-deploy notice pointing authors to `webhook secret`.
491
+ */
492
+ function shouldPrintWebhookSecretNotice(workflows) {
493
+ return workflows.some(wf => {
494
+ if (wf.trigger !== 'webhook') {
495
+ return false;
496
+ }
497
+ const auth = wf.webhook?.auth ?? 'hmac';
498
+ return auth === 'hmac' || auth === 'header';
499
+ });
500
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * dev-shim.mjs — ESM subprocess entrypoint for `solidactions dev`.
3
+ *
4
+ * WHY THIS FILE IS .mjs (NOT .ts):
5
+ * The CLI tsconfig compiles "module": "commonjs". tsc rewrites await import(x)
6
+ * to Promise.resolve(x).then(s => require(s)) in CJS output (confirmed in
7
+ * dist/commands/dev.js:217). A .ts shim compiled by this tsconfig would also
8
+ * produce require(), which runs under tsx's CJS hook — the CJS hook does NOT
9
+ * remap .js→.ts. A hand-written .mjs file bypasses tsc entirely, so its
10
+ * await import() stays a real ESM dynamic import. tsx loads .mjs files under
11
+ * the ESM loader, which DOES remap .js→.ts for all transitive imports.
12
+ *
13
+ * HOW IT IS SPAWNED:
14
+ * npx tsx dist/commands/dev-shim.mjs <userEntry.ts>
15
+ *
16
+ * CONTEXT (from parent, via env var — no secrets to disk):
17
+ * SOLIDACTIONS_DEV_SHIM_CONTEXT: JSON-encoded DevShimContext object (see below).
18
+ *
19
+ * RESULT (written to temp file, path in context):
20
+ * The result file path is passed in context.resultPath. A private temp file
21
+ * (mode 0o600, random UUID name) is written by the parent before spawn; the
22
+ * shim writes its result JSON there; the parent reads it and unlinks it.
23
+ *
24
+ * This file is copied to dist/commands/dev-shim.mjs by the build script.
25
+ */
26
+
27
+ import path from 'path';
28
+ import fs from 'fs';
29
+ import { createRequire } from 'module';
30
+ import { randomUUID } from 'crypto';
31
+
32
+ /**
33
+ * Serialize an unknown error value to a plain JSON-serialisable object.
34
+ * JSON.stringify(new Error('x')) produces {} — this ensures the message
35
+ * is preserved so the CLI's r.error?.message reads correctly.
36
+ *
37
+ * @param {unknown} e
38
+ * @returns {{ message: string; name: string; stack?: string }}
39
+ */
40
+ function serializeError(e) {
41
+ if (e instanceof Error) {
42
+ return { message: e.message, name: e.name, stack: e.stack };
43
+ }
44
+ return { message: String(e), name: 'Error' };
45
+ }
46
+
47
+ async function main() {
48
+ const contextJson = process.env['SOLIDACTIONS_DEV_SHIM_CONTEXT'];
49
+
50
+ if (!contextJson) {
51
+ process.stderr.write('dev-shim: missing SOLIDACTIONS_DEV_SHIM_CONTEXT env var\n');
52
+ process.exit(1);
53
+ }
54
+
55
+ let ctx;
56
+ try {
57
+ ctx = JSON.parse(contextJson);
58
+ } catch (e) {
59
+ process.stderr.write(`dev-shim: failed to parse context JSON: ${e?.message ?? e}\n`);
60
+ process.exit(1);
61
+ }
62
+
63
+ const { entryPath, input, vars, mockBaseUrl, mockApiKey, runUuid, workerSessionId, resultPath } = ctx;
64
+
65
+ // Resolve SDK from the user entry's directory — ensures we hit the same SDK
66
+ // registry instance the workflow populated, not the CLI's own linked copy.
67
+ const _require = createRequire(entryPath);
68
+
69
+ let shimResult;
70
+
71
+ try {
72
+ // Import the user entry file. Under tsx's ESM loader (active because this
73
+ // file is .mjs), dynamic import() correctly remaps .js-extension imports
74
+ // to .ts files for all transitive requires in the user project.
75
+ const mod = await import(entryPath);
76
+
77
+ let descriptor;
78
+
79
+ if (mod?.default && typeof mod.default?.run === 'function') {
80
+ descriptor = mod.default;
81
+ } else {
82
+ const sdkMain = _require.resolve('@solidactions/sdk');
83
+ const registryPath = path.resolve(sdkMain, '..', 'invoke', 'registry.js');
84
+ const registry = _require(registryPath);
85
+ descriptor = registry.__getRegisteredWorkflows()[0];
86
+ }
87
+
88
+ if (!descriptor) {
89
+ shimResult = {
90
+ result: {
91
+ status: 'failed',
92
+ error: serializeError(new Error('no workflow registered after importing the entry file')),
93
+ phase: 'run',
94
+ },
95
+ };
96
+ fs.writeFileSync(resultPath, JSON.stringify(shimResult), { mode: 0o600 });
97
+ process.exit(1);
98
+ return;
99
+ }
100
+
101
+ // Create the run-row BEFORE invoking (descriptor is now available for
102
+ // workflowName — preserves current behaviour where descriptor.name is
103
+ // used at src/commands/dev.ts:319).
104
+ const sdkMainForInvoke = _require.resolve('@solidactions/sdk');
105
+ const httpClientPath = path.resolve(sdkMainForInvoke, '..', 'http_client.js');
106
+ const { HttpClient } = _require(httpClientPath);
107
+ try {
108
+ const client = new HttpClient({ baseUrl: mockBaseUrl, apiKey: mockApiKey });
109
+ await client.post('/runs/status', {
110
+ workflowUUID: runUuid,
111
+ status: 'PENDING',
112
+ workflowName: descriptor.name ?? '',
113
+ workflowClassName: '',
114
+ workflowConfigName: '',
115
+ output: null,
116
+ error: null,
117
+ authenticatedUser: '',
118
+ assumedRole: '',
119
+ authenticatedRoles: [],
120
+ request: {},
121
+ executorId: 'local-dev',
122
+ applicationVersion: '0',
123
+ applicationID: 'local-dev',
124
+ createdAt: Date.now(),
125
+ priority: 0,
126
+ ownerXid: randomUUID(),
127
+ options: {},
128
+ });
129
+ } catch (_e) {
130
+ // Best-effort, symmetric with the parent's swallow.
131
+ }
132
+
133
+ // Build InvokeCtx using the context passed from the parent.
134
+ const invokeCtx = {
135
+ input: JSON.parse(input || '{}'),
136
+ vars: Object.freeze(vars),
137
+ run: {
138
+ triggerId: 'local-dev',
139
+ runUuid,
140
+ runSecret: 'local-dev',
141
+ workerSessionId,
142
+ },
143
+ app: {
144
+ appVersion: '0',
145
+ appId: 'local-dev',
146
+ tenantId: 'local-dev',
147
+ },
148
+ api: {
149
+ url: mockBaseUrl,
150
+ key: mockApiKey,
151
+ },
152
+ mode: 'local',
153
+ };
154
+
155
+ // Invoke via internal SDK path (same resolution as runDev).
156
+ const invokePath = path.resolve(sdkMainForInvoke, '..', 'invoke', 'invoke.js');
157
+ const { invoke } = _require(invokePath);
158
+ const result = await invoke(descriptor, invokeCtx);
159
+
160
+ // Serialize Error objects before JSON.stringify (Error → {} otherwise).
161
+ const serializedResult = {
162
+ ...result,
163
+ ...(result?.error ? { error: serializeError(result.error) } : {}),
164
+ };
165
+
166
+ shimResult = { result: serializedResult };
167
+ } catch (e) {
168
+ // Surface module-resolution failures with an actionable message.
169
+ let errorObj = serializeError(e);
170
+ if (e?.code === 'MODULE_NOT_FOUND' || /Cannot find module/i.test(errorObj.message)) {
171
+ const attempted = e?.requireStack?.[0] ?? entryPath;
172
+ errorObj = {
173
+ ...errorObj,
174
+ message:
175
+ `Cannot find module: ${errorObj.message}\n` +
176
+ `\nThis usually means a TypeScript file uses a .js-extension import (e.g. './lib/foo.js')\n` +
177
+ `but the real file is foo.ts and could not be found by the loader.\n` +
178
+ `\nVerify that your project's tsconfig.json sets "moduleResolution": "NodeNext" or "Bundler"\n` +
179
+ `and that all imported files exist as .ts sources in: ${path.dirname(attempted)}`,
180
+ };
181
+ }
182
+ shimResult = {
183
+ result: { status: 'failed', error: errorObj, phase: 'run' },
184
+ };
185
+ }
186
+
187
+ fs.writeFileSync(resultPath, JSON.stringify(shimResult), { mode: 0o600 });
188
+ process.exit(shimResult.result?.status === 'failed' ? 1 : 0);
189
+ }
190
+
191
+ main().catch((e) => {
192
+ process.stderr.write(`dev-shim: unexpected top-level error: ${e?.message ?? e}\n`);
193
+ process.exit(1);
194
+ });
@@ -42,6 +42,7 @@ exports.dev = dev;
42
42
  const child_process_1 = require("child_process");
43
43
  const path_1 = __importDefault(require("path"));
44
44
  const fs_1 = __importDefault(require("fs"));
45
+ const os_1 = __importDefault(require("os"));
45
46
  const chalk_1 = __importDefault(require("chalk"));
46
47
  const js_yaml_1 = __importDefault(require("js-yaml"));
47
48
  const crypto_1 = require("crypto");
@@ -89,6 +90,127 @@ function findSolidActionsRoot(startPath) {
89
90
  }
90
91
  return null;
91
92
  }
93
+ /**
94
+ * Run the user workflow entry by spawning `npx tsx dist/commands/dev-shim.mjs`.
95
+ *
96
+ * WHY THE CHILD PROCESS (not in-process import):
97
+ * The CLI tsconfig compiles "module": "commonjs", so tsc rewrites await import(x)
98
+ * to require(x) in every .ts file it compiles. tsx's CJS hook does NOT remap
99
+ * .js→.ts in require() calls. A hand-written .mjs shim bypasses tsc entirely,
100
+ * so its import() stays a real ESM dynamic import. When tsx loads .mjs, it uses
101
+ * the ESM loader, which DOES remap .js→.ts for all transitive imports.
102
+ *
103
+ * SECRETS: context is passed as a JSON env var — OAuth proxyToken values never
104
+ * touch disk. The result uses a private temp file (mode 0o600, UUID name).
105
+ */
106
+ async function runDevViaShim(entryPath, input, vars, mockBaseUrl, runUuid, workerSessionId, stdoutLines, stderrLines) {
107
+ const shimPath = path_1.default.resolve(__dirname, 'dev-shim.mjs');
108
+ // Private temp file for the result (mode 0o600, UUID name).
109
+ const resultPath = path_1.default.join(os_1.default.tmpdir(), `sa-dev-result-${runUuid}-${(0, crypto_1.randomUUID)()}.json`);
110
+ // Write placeholder so the shim can overwrite it.
111
+ fs_1.default.writeFileSync(resultPath, '', { mode: 0o600 });
112
+ const shimCtx = {
113
+ entryPath,
114
+ input,
115
+ vars,
116
+ mockBaseUrl,
117
+ mockApiKey: 'local-dev',
118
+ runUuid,
119
+ workerSessionId,
120
+ resultPath,
121
+ };
122
+ try {
123
+ // Use async spawn (NOT spawnSync): the SDK mock backend runs in THIS
124
+ // process's event loop, and the shim makes HTTP calls back to it
125
+ // (run-row creation, step/sleep/recv routes). spawnSync would block the
126
+ // parent's event loop, deadlocking the mock server — so we await an
127
+ // async child and pump its stdout/stderr while the loop stays live.
128
+ let timedOut = false;
129
+ const spawnError = await new Promise((resolve) => {
130
+ const child = (0, child_process_1.spawn)('npx', ['tsx', shimPath], {
131
+ stdio: ['ignore', 'pipe', 'pipe'],
132
+ env: {
133
+ ...process.env,
134
+ SOLIDACTIONS_DEV_SHIM_CONTEXT: JSON.stringify(shimCtx),
135
+ },
136
+ });
137
+ let stdoutBuf = '';
138
+ let stderrBuf = '';
139
+ child.stdout.setEncoding('utf8');
140
+ child.stderr.setEncoding('utf8');
141
+ child.stdout.on('data', (chunk) => { stdoutBuf += chunk; });
142
+ child.stderr.on('data', (chunk) => { stderrBuf += chunk; });
143
+ const timer = setTimeout(() => {
144
+ timedOut = true;
145
+ child.kill('SIGKILL');
146
+ }, 120_000);
147
+ child.on('error', (e) => {
148
+ clearTimeout(timer);
149
+ if (stdoutBuf.trim()) {
150
+ stdoutLines.push(stdoutBuf.trimEnd());
151
+ }
152
+ if (stderrBuf.trim()) {
153
+ stderrLines.push(stderrBuf.trimEnd());
154
+ }
155
+ resolve(e);
156
+ });
157
+ child.on('close', () => {
158
+ clearTimeout(timer);
159
+ if (stdoutBuf.trim()) {
160
+ stdoutLines.push(stdoutBuf.trimEnd());
161
+ }
162
+ if (stderrBuf.trim()) {
163
+ stderrLines.push(stderrBuf.trimEnd());
164
+ }
165
+ resolve(undefined);
166
+ });
167
+ });
168
+ if (spawnError) {
169
+ const code = spawnError.code;
170
+ const msg = code === 'ENOENT'
171
+ ? 'npx not found. Make sure Node.js is installed.'
172
+ : `Failed to spawn tsx shim: ${spawnError.message}`;
173
+ return {
174
+ result: { status: 'failed', error: { message: msg, name: 'Error' }, phase: 'run' },
175
+ };
176
+ }
177
+ let resultContent = '';
178
+ try {
179
+ resultContent = fs_1.default.readFileSync(resultPath, 'utf8');
180
+ }
181
+ catch {
182
+ // shim crashed before writing — stderr already captured above
183
+ return {
184
+ result: {
185
+ status: 'failed',
186
+ error: { message: 'dev-shim did not write a result file (check stderr above)', name: 'Error' },
187
+ phase: 'run',
188
+ },
189
+ };
190
+ }
191
+ if (!resultContent.trim()) {
192
+ const msg = timedOut
193
+ ? 'dev-shim timed out after 120s'
194
+ : 'dev-shim wrote an empty result file (check stderr above)';
195
+ return {
196
+ result: {
197
+ status: 'failed',
198
+ error: { message: msg, name: 'Error' },
199
+ phase: 'run',
200
+ },
201
+ };
202
+ }
203
+ const shimResult = JSON.parse(resultContent);
204
+ return { result: shimResult.result };
205
+ }
206
+ finally {
207
+ // Clean up result temp file on all exit paths.
208
+ try {
209
+ fs_1.default.unlinkSync(resultPath);
210
+ }
211
+ catch { /* ignore */ }
212
+ }
213
+ }
92
214
  /**
93
215
  * Run a workflow entry file in local-dev mode:
94
216
  * 1. Fetch declared vars + connections from the platform (via api seam).
@@ -208,8 +330,33 @@ async function runDev(opts) {
208
330
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
331
  const { createMockServer } = _require(sdkTestingMain);
210
332
  const mockServer = await createMockServer();
333
+ // Pre-generate run IDs so they can be passed to the shim (for .ts entries)
334
+ // or used in-process (for .js/.mjs entries).
335
+ const runUuid = (0, crypto_1.randomUUID)();
336
+ const workerSessionId = (0, crypto_1.randomUUID)();
211
337
  try {
212
- // 7. Import the workflow entry file. Prefer the module's default export
338
+ // 7. Load and invoke the workflow.
339
+ //
340
+ // For .ts entries: spawn `npx tsx dist/commands/dev-shim.mjs`. The .mjs shim
341
+ // is NOT compiled by tsc, so its import() stays a real ESM dynamic import.
342
+ // tsx's ESM loader remaps .js-extension imports to .ts files, fixing the
343
+ // Cannot-find-module bug on multi-file NodeNext projects.
344
+ //
345
+ // The shim creates the run-row (it has the descriptor name) and invokes.
346
+ // stdio: pipe — child stdout/stderr are captured into stdoutLines/stderrLines.
347
+ //
348
+ // For .js/.mjs entries: original in-process import path (no change).
349
+ const isTs = /\.(ts|tsx|mts|cts)$/.test(entryPath);
350
+ if (isTs) {
351
+ const shimInvokeResult = await runDevViaShim(entryPath, opts.input, vars, mockServer.baseUrl, runUuid, workerSessionId, stdoutLines, stderrLines);
352
+ return {
353
+ stdout: stdoutLines.join('\n'),
354
+ stderr: stderrLines.join('\n'),
355
+ result: shimInvokeResult.result,
356
+ };
357
+ }
358
+ // JS/MJS path: in-process import (original behaviour, no change).
359
+ // 8. Import the workflow entry file. Prefer the module's default export
213
360
  // (a WorkflowDescriptor returned by defineWorkflow) so that repeated
214
361
  // runDev() calls in the same test process don't need to clear and
215
362
  // re-register from a cached module (cached CJS modules don't re-run
@@ -236,8 +383,7 @@ async function runDev(opts) {
236
383
  result: { status: 'failed', error: new Error('no workflow registered'), phase: 'run' },
237
384
  };
238
385
  }
239
- // 8. Build InvokeCtx.
240
- const runUuid = (0, crypto_1.randomUUID)();
386
+ // 9. Build InvokeCtx.
241
387
  const ctx = {
242
388
  input: JSON.parse(opts.input || '{}'),
243
389
  vars: Object.freeze(vars),
@@ -245,7 +391,7 @@ async function runDev(opts) {
245
391
  triggerId: 'local-dev',
246
392
  runUuid,
247
393
  runSecret: 'local-dev',
248
- workerSessionId: (0, crypto_1.randomUUID)(),
394
+ workerSessionId,
249
395
  },
250
396
  app: {
251
397
  appVersion: '0',
@@ -258,7 +404,7 @@ async function runDev(opts) {
258
404
  },
259
405
  mode: 'local',
260
406
  };
261
- // 9. Create the run-row BEFORE invoking. invoke() itself never creates
407
+ // 10. Create the run-row BEFORE invoking. invoke() itself never creates
262
408
  // the durable run record — the production launcher (SolidActions.run
263
409
  // → #initOneShotStatusRow) does that first, and step/sleep/recv
264
410
  // sub-routes (`/runs/status/<id>/...`) 404 ("Workflow not found")
@@ -296,7 +442,7 @@ async function runDev(opts) {
296
442
  // Best-effort, symmetric with #initOneShotStatusRow's swallow.
297
443
  err(`failed to create local run-row: ${e?.message ?? e}`);
298
444
  }
299
- // 10. Invoke via internal SDK path (invoke() is not in the public index).
445
+ // 11. Invoke via internal SDK path (invoke() is not in the public index).
300
446
  const invokePath = path_1.default.resolve(sdkMainForInvoke, '..', 'invoke', 'invoke.js');
301
447
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
448
  const { invoke } = _require(invokePath);
@@ -320,13 +466,25 @@ const TSX_REEXEC_GUARD = 'SOLIDACTIONS_DEV_TSX_REEXEC';
320
466
  * Detect whether the current Node process already has a TypeScript loader
321
467
  * registered (tsx / ts-node). When true, `await import('<file>.ts')` works
322
468
  * in-process and we can run `runDev` directly without re-exec.
469
+ *
470
+ * NOTE: Boolean(process._preload_modules) is intentionally NOT used here —
471
+ * an empty array is truthy, causing a false positive that always skips the
472
+ * re-exec path even when no TypeScript loader is active.
323
473
  */
324
474
  function hasTsLoader() {
325
475
  if (process.env[TSX_REEXEC_GUARD] === '1') {
326
476
  return true;
327
477
  }
328
478
  const execArgv = process.execArgv.join(' ');
329
- return /tsx|ts-node/.test(execArgv) || Boolean(process._preload_modules);
479
+ if (/tsx|ts-node/.test(execArgv)) {
480
+ return true;
481
+ }
482
+ // Check _preload_modules for tsx/ts-node entries (non-empty array check).
483
+ const preload = process._preload_modules;
484
+ if (Array.isArray(preload) && preload.length > 0) {
485
+ return preload.some((m) => typeof m === 'string' && /tsx|ts-node/i.test(m));
486
+ }
487
+ return false;
330
488
  }
331
489
  /**
332
490
  * Re-exec the CLI under `npx tsx` so that `runDev`'s `await import()` of a
@@ -8,8 +8,13 @@ const axios_1 = __importDefault(require("axios"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const prompts_1 = __importDefault(require("prompts"));
10
10
  const api_1 = require("../utils/api");
11
+ const env_1 = require("../utils/env");
11
12
  async function envMap(projectName, projectKey, globalKey, options = {}) {
12
13
  const config = await (0, api_1.requireConfigWithWorkspace)();
14
+ if ((0, env_1.isReservedEnvName)(projectKey)) {
15
+ console.error(chalk_1.default.red((0, env_1.reservedEnvNameError)(projectKey)));
16
+ process.exit(1);
17
+ }
13
18
  console.log(chalk_1.default.blue(`Mapping global variable "${globalKey}" to project key "${projectKey}" in "${projectName}"...`));
14
19
  try {
15
20
  // First, get the global variable ID by key
@@ -72,6 +72,15 @@ async function envPush(projectName, sourcePath, options = {}) {
72
72
  }
73
73
  process.exit(0);
74
74
  }
75
+ // Hard-reject reserved names among the keys that would actually be pushed
76
+ // (the server rejects them too — fail here, before any network call).
77
+ const reservedKeys = keysToProcess.filter(env_1.isReservedEnvName);
78
+ if (reservedKeys.length > 0) {
79
+ console.error(chalk_1.default.red(`Refusing to push — reserved ${env_1.RESERVED_ENV_PREFIX} names found: ${reservedKeys.join(', ')}`));
80
+ console.error(chalk_1.default.red('These names are set by the platform at runtime (API credentials, run context) and a custom variable would clobber them, causing authentication failures. ' +
81
+ `Rename them (e.g. "${reservedKeys[0].replace(/^SOLIDACTIONS_/, 'MY_')}") and retry. Nothing was pushed.`));
82
+ process.exit(1);
83
+ }
75
84
  // Build project slug
76
85
  const projectSlug = environment === 'production'
77
86
  ? projectName