@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.
- package/dist/commands/deploy.js +78 -11
- package/dist/commands/dev-shim.mjs +194 -0
- package/dist/commands/dev.js +165 -7
- package/dist/commands/env-map.js +5 -0
- package/dist/commands/env-push.js +9 -0
- package/dist/commands/env-set.js +33 -0
- package/dist/commands/init.js +7 -4
- package/dist/commands/login.js +22 -8
- package/dist/commands/project-logs.js +26 -3
- package/dist/commands/run-list.js +41 -19
- package/dist/commands/run-view.js +54 -4
- package/dist/commands/schedule-list.js +4 -2
- package/dist/commands/schedule-set.js +42 -10
- package/dist/commands/webhook-secret.js +74 -0
- package/dist/index.js +23 -4
- package/dist/utils/env.js +19 -0
- package/dist/utils/skills.js +19 -0
- package/package.json +2 -2
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
-
//
|
|
156
|
-
//
|
|
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
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
});
|
package/dist/commands/dev.js
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
package/dist/commands/env-map.js
CHANGED
|
@@ -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
|