@rudderhq/agent-runtime-utils 0.4.6-canary.9 → 0.5.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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/rudder-mcp-contract.d.ts +1028 -0
- package/dist/rudder-mcp-contract.d.ts.map +1 -0
- package/dist/rudder-mcp-contract.js +257 -0
- package/dist/rudder-mcp-contract.js.map +1 -0
- package/dist/rudder-mcp-fingerprint.d.ts +4 -0
- package/dist/rudder-mcp-fingerprint.d.ts.map +1 -0
- package/dist/rudder-mcp-fingerprint.js +23 -0
- package/dist/rudder-mcp-fingerprint.js.map +1 -0
- package/dist/rudder-mcp-preflight.d.ts +12 -0
- package/dist/rudder-mcp-preflight.d.ts.map +1 -0
- package/dist/rudder-mcp-preflight.js +324 -0
- package/dist/rudder-mcp-preflight.js.map +1 -0
- package/dist/rudder-mcp-preflight.test.d.ts +2 -0
- package/dist/rudder-mcp-preflight.test.d.ts.map +1 -0
- package/dist/rudder-mcp-preflight.test.js +315 -0
- package/dist/rudder-mcp-preflight.test.js.map +1 -0
- package/dist/rudder-mcp-server.d.ts.map +1 -1
- package/dist/rudder-mcp-server.js +3 -0
- package/dist/rudder-mcp-server.js.map +1 -1
- package/dist/rudder-mcp-tool-descriptors.generated.d.ts +620 -0
- package/dist/rudder-mcp-tool-descriptors.generated.d.ts.map +1 -0
- package/dist/rudder-mcp-tool-descriptors.generated.js +699 -0
- package/dist/rudder-mcp-tool-descriptors.generated.js.map +1 -0
- package/dist/rudder-mcp.d.ts +32 -2
- package/dist/rudder-mcp.d.ts.map +1 -1
- package/dist/rudder-mcp.js +28 -9
- package/dist/rudder-mcp.js.map +1 -1
- package/dist/rudder-mcp.test.js +38 -2
- package/dist/rudder-mcp.test.js.map +1 -1
- package/dist/server-utils.cli.d.ts +41 -3
- package/dist/server-utils.cli.d.ts.map +1 -1
- package/dist/server-utils.cli.js +307 -23
- package/dist/server-utils.cli.js.map +1 -1
- package/dist/server-utils.prompts.d.ts +8 -6
- package/dist/server-utils.prompts.d.ts.map +1 -1
- package/dist/server-utils.prompts.js +77 -24
- package/dist/server-utils.prompts.js.map +1 -1
- package/dist/server-utils.prompts.test.js +97 -1
- package/dist/server-utils.prompts.test.js.map +1 -1
- package/dist/server-utils.test.js +582 -5
- package/dist/server-utils.test.js.map +1 -1
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server-utils.cli.js
CHANGED
|
@@ -6,6 +6,29 @@ import path from "node:path";
|
|
|
6
6
|
import { defaultPathForPlatform, fileExists, quoteForCmd, resolveCommandPath, resolveSpawnTarget } from "./server-utils.instructions.js";
|
|
7
7
|
import { appendWithCap, asString, buildManagedSkillOrigin, compactSkillText, DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES, DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS, isChildProcessAlive, isMaintainerOnlySkillTarget, parseObject, readSkillMetadataFromDirectory, resolveInstalledEntryTarget, RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES, runningProcesses, skillLocationLabel } from "./server-utils.process.js";
|
|
8
8
|
const LOCAL_CLI_CREDENTIAL_AUTH_CHECK_TIMEOUT_MS = 3000;
|
|
9
|
+
const RUDDER_DESKTOP_CLI_ENTRY_ENV = "RUDDER_DESKTOP_CLI_ENTRY";
|
|
10
|
+
export const OPERATOR_INTERRUPT_ABORT_REASON_KIND = "operator_interrupt";
|
|
11
|
+
export function createOperatorInterruptAbortReason(hardDeadlineMs) {
|
|
12
|
+
if (!Number.isFinite(hardDeadlineMs) || hardDeadlineMs <= 0) {
|
|
13
|
+
throw new RangeError("operator interrupt hardDeadlineMs must be a positive finite number");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
kind: OPERATOR_INTERRUPT_ABORT_REASON_KIND,
|
|
17
|
+
hardDeadlineMs: Math.max(1, Math.floor(hardDeadlineMs)),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function operatorInterruptHardDeadlineMs(reason) {
|
|
21
|
+
if (!reason || typeof reason !== "object" || Array.isArray(reason))
|
|
22
|
+
return null;
|
|
23
|
+
const candidate = reason;
|
|
24
|
+
if (candidate.kind !== OPERATOR_INTERRUPT_ABORT_REASON_KIND)
|
|
25
|
+
return null;
|
|
26
|
+
if (typeof candidate.hardDeadlineMs !== "number" || !Number.isFinite(candidate.hardDeadlineMs))
|
|
27
|
+
return null;
|
|
28
|
+
if (candidate.hardDeadlineMs <= 0)
|
|
29
|
+
return null;
|
|
30
|
+
return Math.max(1, Math.floor(candidate.hardDeadlineMs));
|
|
31
|
+
}
|
|
9
32
|
export function ensurePathInEnv(env) {
|
|
10
33
|
if (typeof env.PATH === "string" && env.PATH.length > 0)
|
|
11
34
|
return env;
|
|
@@ -26,7 +49,7 @@ export function prependPathEntry(env, entry) {
|
|
|
26
49
|
[pathKey]: current.length > 0 ? `${entry}${delimiter}${current}` : entry,
|
|
27
50
|
};
|
|
28
51
|
}
|
|
29
|
-
function killChildProcessTree(child, force) {
|
|
52
|
+
export function killChildProcessTree(child, force) {
|
|
30
53
|
if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) {
|
|
31
54
|
const args = ["/pid", String(child.pid), "/t"];
|
|
32
55
|
if (force)
|
|
@@ -38,7 +61,39 @@ function killChildProcessTree(child, force) {
|
|
|
38
61
|
killer.on("error", () => { });
|
|
39
62
|
return;
|
|
40
63
|
}
|
|
41
|
-
|
|
64
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
65
|
+
if (typeof child.pid === "number" && child.pid > 0) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(-child.pid, signal);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Fall back to the direct child if its process group is already gone.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
child.kill(signal);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// The process may have exited between the liveness check and signal.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isChildProcessTreeAlive(child) {
|
|
82
|
+
if (process.platform === "win32")
|
|
83
|
+
return isChildProcessAlive(child);
|
|
84
|
+
const pid = child.pid;
|
|
85
|
+
if (typeof pid !== "number" || pid <= 0)
|
|
86
|
+
return false;
|
|
87
|
+
try {
|
|
88
|
+
process.kill(-pid, 0);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const code = error instanceof Error && "code" in error ? error.code : null;
|
|
93
|
+
if (code === "EPERM")
|
|
94
|
+
return true;
|
|
95
|
+
return isChildProcessAlive(child);
|
|
96
|
+
}
|
|
42
97
|
}
|
|
43
98
|
export async function findAncestorWithFile(startDir, relativePath, maxDepth = 12) {
|
|
44
99
|
let current = path.resolve(startDir);
|
|
@@ -56,12 +111,69 @@ export async function findAncestorWithFile(startDir, relativePath, maxDepth = 12
|
|
|
56
111
|
export function shellQuote(arg) {
|
|
57
112
|
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
58
113
|
}
|
|
114
|
+
async function readPackageVersion(filePath, expectedName) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
117
|
+
if (expectedName && parsed.name !== expectedName)
|
|
118
|
+
return null;
|
|
119
|
+
return typeof parsed.version === "string" && parsed.version.trim().length > 0 ? parsed.version.trim() : null;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function cliVersionBesideEntry(cliEntry) {
|
|
126
|
+
const directory = path.dirname(cliEntry);
|
|
127
|
+
return await readPackageVersion(path.join(directory, "rudder-cli-package.json"), "@rudderhq/cli")
|
|
128
|
+
?? await readPackageVersion(path.join(directory, "package.json"), "@rudderhq/cli");
|
|
129
|
+
}
|
|
130
|
+
async function resolveExplicitDesktopCliEntry(env = process.env) {
|
|
131
|
+
const configured = env[RUDDER_DESKTOP_CLI_ENTRY_ENV]?.trim();
|
|
132
|
+
if (!configured)
|
|
133
|
+
return null;
|
|
134
|
+
const cliEntry = path.resolve(configured);
|
|
135
|
+
return await fileExists(cliEntry) ? cliEntry : null;
|
|
136
|
+
}
|
|
137
|
+
async function resolveDesktopExecutableBesideCliEntry(cliEntry) {
|
|
138
|
+
const resourcesDir = path.dirname(path.dirname(cliEntry));
|
|
139
|
+
const appRoot = path.dirname(resourcesDir);
|
|
140
|
+
const executable = process.platform === "darwin"
|
|
141
|
+
? path.join(appRoot, "MacOS", "Rudder")
|
|
142
|
+
: path.join(appRoot, process.platform === "win32" ? "Rudder.exe" : "Rudder");
|
|
143
|
+
return await fileExists(executable) ? executable : null;
|
|
144
|
+
}
|
|
145
|
+
export function resolveDesktopCliSpawnTarget(cliEntry, executable, platform = process.platform, nodeRunner = null) {
|
|
146
|
+
if (platform === "win32" && nodeRunner) {
|
|
147
|
+
return {
|
|
148
|
+
command: executable,
|
|
149
|
+
args: [nodeRunner],
|
|
150
|
+
env: { ELECTRON_RUN_AS_NODE: "1" },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
command: executable,
|
|
155
|
+
args: ["--desktop-cli"],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
59
158
|
export async function resolveRudderCliShimTarget(moduleDir) {
|
|
159
|
+
const explicitDesktopCli = await resolveExplicitDesktopCliEntry();
|
|
160
|
+
if (explicitDesktopCli) {
|
|
161
|
+
const executable = await resolveDesktopExecutableBesideCliEntry(explicitDesktopCli) ?? process.execPath;
|
|
162
|
+
const nodeRunner = path.join(path.dirname(explicitDesktopCli), "desktop-cli-runner.js");
|
|
163
|
+
return {
|
|
164
|
+
...resolveDesktopCliSpawnTarget(explicitDesktopCli, executable, process.platform, await fileExists(nodeRunner) ? nodeRunner : null),
|
|
165
|
+
provenance: "desktop_bundle",
|
|
166
|
+
version: await cliVersionBesideEntry(explicitDesktopCli),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
60
169
|
const packagedCli = await findAncestorWithFile(moduleDir, "desktop-cli.js");
|
|
61
170
|
if (packagedCli) {
|
|
171
|
+
const runtimeMetadata = await findAncestorWithFile(moduleDir, "runtime.json");
|
|
62
172
|
return {
|
|
63
173
|
command: process.execPath,
|
|
64
174
|
args: [packagedCli],
|
|
175
|
+
provenance: runtimeMetadata ? "external_runtime" : "desktop_bundle",
|
|
176
|
+
version: await cliVersionBesideEntry(packagedCli),
|
|
65
177
|
};
|
|
66
178
|
}
|
|
67
179
|
const repoRoot = await findAncestorWithFile(moduleDir, path.join("cli", "src", "index.ts"));
|
|
@@ -74,6 +186,8 @@ export async function resolveRudderCliShimTarget(moduleDir) {
|
|
|
74
186
|
return {
|
|
75
187
|
command: process.execPath,
|
|
76
188
|
args: [tsxEntry, cliSource],
|
|
189
|
+
provenance: "repo",
|
|
190
|
+
version: await readPackageVersion(path.join(rootDir, "cli", "package.json"), "@rudderhq/cli"),
|
|
77
191
|
};
|
|
78
192
|
}
|
|
79
193
|
const builtCliEntry = path.join(rootDir, "cli", "dist", "index.js");
|
|
@@ -81,13 +195,15 @@ export async function resolveRudderCliShimTarget(moduleDir) {
|
|
|
81
195
|
return {
|
|
82
196
|
command: process.execPath,
|
|
83
197
|
args: [builtCliEntry],
|
|
198
|
+
provenance: "repo",
|
|
199
|
+
version: await readPackageVersion(path.join(rootDir, "cli", "package.json"), "@rudderhq/cli"),
|
|
84
200
|
};
|
|
85
201
|
}
|
|
86
202
|
return null;
|
|
87
203
|
}
|
|
88
204
|
export async function materializeRudderCliShim(target) {
|
|
89
205
|
const hash = createHash("sha1")
|
|
90
|
-
.update(JSON.stringify({ command: target.command, args: target.args, platform: process.platform }))
|
|
206
|
+
.update(JSON.stringify({ command: target.command, args: target.args, env: target.env, platform: process.platform }))
|
|
91
207
|
.digest("hex")
|
|
92
208
|
.slice(0, 12);
|
|
93
209
|
const shimDir = path.join(os.tmpdir(), "rudder-cli-shims", hash);
|
|
@@ -95,7 +211,8 @@ export async function materializeRudderCliShim(target) {
|
|
|
95
211
|
if (process.platform === "win32") {
|
|
96
212
|
const shimPath = path.join(shimDir, "rudder.cmd");
|
|
97
213
|
const commandLine = [quoteForCmd(target.command), ...target.args.map(quoteForCmd), "%*"].join(" ");
|
|
98
|
-
|
|
214
|
+
const electronNodeMode = target.env?.ELECTRON_RUN_AS_NODE === "1";
|
|
215
|
+
await fs.writeFile(shimPath, `@echo off\r\n${electronNodeMode ? "set \"ELECTRON_RUN_AS_NODE=1\"\r\n" : ""}${commandLine}\r\n`, "utf8");
|
|
99
216
|
return shimPath;
|
|
100
217
|
}
|
|
101
218
|
const shimPath = path.join(shimDir, "rudder");
|
|
@@ -347,19 +464,31 @@ export function readRudderSkillSyncPreference(config) {
|
|
|
347
464
|
};
|
|
348
465
|
}
|
|
349
466
|
export function canonicalizeDesiredRudderSkillReference(reference, availableEntries) {
|
|
350
|
-
const normalizedReference = reference.trim().toLowerCase()
|
|
467
|
+
const normalizedReference = reference.trim().toLowerCase();
|
|
351
468
|
if (!normalizedReference)
|
|
352
469
|
return "";
|
|
353
470
|
const exactKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === normalizedReference);
|
|
354
471
|
if (exactKey)
|
|
355
472
|
return exactKey.key;
|
|
356
|
-
|
|
473
|
+
if (normalizedReference === "rudder"
|
|
474
|
+
|| normalizedReference === "rudder/rudder"
|
|
475
|
+
|| normalizedReference === "bundled:rudder/rudder") {
|
|
476
|
+
const canonicalRudderDocsEntry = availableEntries.find((entry) => entry.key.trim().toLowerCase() === "rudder/rudder-docs"
|
|
477
|
+
|| entry.key.trim().toLowerCase() === "bundled:rudder/rudder-docs"
|
|
478
|
+
|| entry.runtimeName?.trim().toLowerCase() === "rudder-docs");
|
|
479
|
+
return canonicalRudderDocsEntry?.key ?? "rudder/rudder-docs";
|
|
480
|
+
}
|
|
481
|
+
const lookupReference = normalizedReference.replace(/^rudder\/rudder\//, "rudder/");
|
|
482
|
+
const legacyNestedKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === lookupReference);
|
|
483
|
+
if (legacyNestedKey)
|
|
484
|
+
return legacyNestedKey.key;
|
|
485
|
+
const byRuntimeName = availableEntries.filter((entry) => typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === lookupReference);
|
|
357
486
|
if (byRuntimeName.length === 1)
|
|
358
487
|
return byRuntimeName[0].key;
|
|
359
|
-
const slugMatches = availableEntries.filter((entry) => entry.key.trim().toLowerCase().split("/").pop() ===
|
|
488
|
+
const slugMatches = availableEntries.filter((entry) => entry.key.trim().toLowerCase().split("/").pop() === lookupReference);
|
|
360
489
|
if (slugMatches.length === 1)
|
|
361
490
|
return slugMatches[0].key;
|
|
362
|
-
return
|
|
491
|
+
return lookupReference;
|
|
363
492
|
}
|
|
364
493
|
export function resolveRudderDesiredSkillNames(config, availableEntries) {
|
|
365
494
|
const preference = readRudderSkillSyncPreference(config);
|
|
@@ -368,6 +497,14 @@ export function resolveRudderDesiredSkillNames(config, availableEntries) {
|
|
|
368
497
|
.filter(Boolean);
|
|
369
498
|
return Array.from(new Set(desiredSkills));
|
|
370
499
|
}
|
|
500
|
+
export function filterRudderDesiredSkillsForBrowserCapability(availableEntries, desiredSkills, browserEnabled) {
|
|
501
|
+
if (browserEnabled)
|
|
502
|
+
return [...desiredSkills];
|
|
503
|
+
const browserSkillKeys = new Set(availableEntries
|
|
504
|
+
.filter((entry) => entry.runtimeName?.trim().toLowerCase() === "browser")
|
|
505
|
+
.map((entry) => entry.key));
|
|
506
|
+
return desiredSkills.filter((skill) => !browserSkillKeys.has(skill));
|
|
507
|
+
}
|
|
371
508
|
export function writeRudderSkillSyncPreference(config, desiredSkills) {
|
|
372
509
|
const next = { ...config };
|
|
373
510
|
const raw = next.rudderSkillSync;
|
|
@@ -650,6 +787,120 @@ export async function ensureRudderSkillSymlink(source, target, linkSkill = (link
|
|
|
650
787
|
export async function removeMaintainerOnlySkillSymlinks(skillsHome, allowedSkillNames) {
|
|
651
788
|
return removeUnselectedRudderSkillSymlinks(skillsHome, allowedSkillNames);
|
|
652
789
|
}
|
|
790
|
+
function isFileSystemErrorCode(error, code) {
|
|
791
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
792
|
+
}
|
|
793
|
+
function sameFileSystemEntry(left, right) {
|
|
794
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
795
|
+
}
|
|
796
|
+
export async function cleanupLegacyRudderDocsManagedEntry(skillsHome, selectedEntries) {
|
|
797
|
+
const targetPath = path.join(path.resolve(skillsHome), "rudder");
|
|
798
|
+
const canonicalEntry = Array.from(selectedEntries).find((entry) => (entry.key === "rudder/rudder-docs" || entry.key === "bundled:rudder/rudder-docs")
|
|
799
|
+
&& entry.runtimeName === "rudder-docs");
|
|
800
|
+
if (!canonicalEntry) {
|
|
801
|
+
return { state: "not_applicable", targetPath, legacySourcePath: null, kind: null };
|
|
802
|
+
}
|
|
803
|
+
const legacySourcePath = path.join(path.dirname(path.resolve(canonicalEntry.source)), "rudder");
|
|
804
|
+
let existing;
|
|
805
|
+
try {
|
|
806
|
+
existing = await fs.lstat(targetPath);
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
810
|
+
return { state: "absent", targetPath, legacySourcePath, kind: null };
|
|
811
|
+
}
|
|
812
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "other" };
|
|
813
|
+
}
|
|
814
|
+
if (existing.isSymbolicLink()) {
|
|
815
|
+
const linkedPath = await fs.readlink(targetPath).catch(() => null);
|
|
816
|
+
if (linkedPath === null) {
|
|
817
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
818
|
+
}
|
|
819
|
+
const resolvedLinkedPath = path.resolve(path.dirname(targetPath), linkedPath);
|
|
820
|
+
if (resolvedLinkedPath !== legacySourcePath) {
|
|
821
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
await fs.stat(resolvedLinkedPath);
|
|
825
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
if (!isFileSystemErrorCode(error, "ENOENT")) {
|
|
829
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Ownership evidence is path-based, so revalidate inode, type, and target immediately before deletion.
|
|
833
|
+
const revalidated = await fs.lstat(targetPath).catch(() => null);
|
|
834
|
+
if (!revalidated?.isSymbolicLink() || !sameFileSystemEntry(existing, revalidated)) {
|
|
835
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
836
|
+
}
|
|
837
|
+
const revalidatedLink = await fs.readlink(targetPath).catch(() => null);
|
|
838
|
+
if (revalidatedLink === null
|
|
839
|
+
|| path.resolve(path.dirname(targetPath), revalidatedLink) !== legacySourcePath) {
|
|
840
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
841
|
+
}
|
|
842
|
+
const finalEntry = await fs.lstat(targetPath).catch(() => null);
|
|
843
|
+
if (!finalEntry?.isSymbolicLink() || !sameFileSystemEntry(existing, finalEntry)) {
|
|
844
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
await fs.stat(legacySourcePath);
|
|
848
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
if (!isFileSystemErrorCode(error, "ENOENT")) {
|
|
852
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "symlink" };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
await fs.unlink(targetPath);
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
return {
|
|
860
|
+
state: "failed",
|
|
861
|
+
targetPath,
|
|
862
|
+
legacySourcePath,
|
|
863
|
+
kind: "symlink",
|
|
864
|
+
detail: `unlink failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
return { state: "removed", targetPath, legacySourcePath, kind: "symlink" };
|
|
868
|
+
}
|
|
869
|
+
if (existing.isDirectory()) {
|
|
870
|
+
const materializedSource = await readRudderMaterializedSkillSource(targetPath);
|
|
871
|
+
if (materializedSource === legacySourcePath) {
|
|
872
|
+
// A matching manifest can move with a replacement; inode and provenance must both remain stable.
|
|
873
|
+
const revalidated = await fs.lstat(targetPath).catch(() => null);
|
|
874
|
+
if (!revalidated?.isDirectory() || !sameFileSystemEntry(existing, revalidated)) {
|
|
875
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
|
|
876
|
+
}
|
|
877
|
+
const revalidatedSource = await readRudderMaterializedSkillSource(targetPath);
|
|
878
|
+
if (revalidatedSource !== legacySourcePath) {
|
|
879
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
|
|
880
|
+
}
|
|
881
|
+
const finalEntry = await fs.lstat(targetPath).catch(() => null);
|
|
882
|
+
if (!finalEntry?.isDirectory() || !sameFileSystemEntry(existing, finalEntry)) {
|
|
883
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
return {
|
|
890
|
+
state: "failed",
|
|
891
|
+
targetPath,
|
|
892
|
+
legacySourcePath,
|
|
893
|
+
kind: "directory",
|
|
894
|
+
detail: `recursive removal failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return { state: "removed", targetPath, legacySourcePath, kind: "directory" };
|
|
898
|
+
}
|
|
899
|
+
return { state: "collision", targetPath, legacySourcePath, kind: "directory" };
|
|
900
|
+
}
|
|
901
|
+
const kind = existing.isFile() ? "file" : "other";
|
|
902
|
+
return { state: "collision", targetPath, legacySourcePath, kind };
|
|
903
|
+
}
|
|
653
904
|
async function readRudderMaterializedSkillSource(target) {
|
|
654
905
|
const manifestPath = path.join(target, ".rudder", "materialized-skill.json");
|
|
655
906
|
const raw = await fs.readFile(manifestPath, "utf8").catch(() => null);
|
|
@@ -722,6 +973,7 @@ export async function runChildProcess(runId, command, args, opts) {
|
|
|
722
973
|
const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
|
|
723
974
|
return new Promise((resolve, reject) => {
|
|
724
975
|
const rawMerged = { ...process.env, ...opts.env };
|
|
976
|
+
delete rawMerged.RUDDER_DESKTOP_CLI_ENTRY;
|
|
725
977
|
const requestedHome = typeof opts.env.HOME === "string" && opts.env.HOME.trim().length > 0
|
|
726
978
|
? path.resolve(opts.env.HOME)
|
|
727
979
|
: null;
|
|
@@ -777,6 +1029,7 @@ export async function runChildProcess(runId, command, args, opts) {
|
|
|
777
1029
|
}
|
|
778
1030
|
const child = spawn(target.command, target.args, {
|
|
779
1031
|
cwd: opts.cwd,
|
|
1032
|
+
detached: process.platform !== "win32",
|
|
780
1033
|
env: mergedEnv,
|
|
781
1034
|
shell: false,
|
|
782
1035
|
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
@@ -797,43 +1050,70 @@ export async function runChildProcess(runId, command, args, opts) {
|
|
|
797
1050
|
let stdout = "";
|
|
798
1051
|
let stderr = "";
|
|
799
1052
|
let logChain = Promise.resolve();
|
|
1053
|
+
let operatorInterrupted = false;
|
|
1054
|
+
let forceKillTimer = null;
|
|
1055
|
+
let forceKillAt = Number.POSITIVE_INFINITY;
|
|
1056
|
+
const clearForceKillTimer = () => {
|
|
1057
|
+
if (forceKillTimer)
|
|
1058
|
+
clearTimeout(forceKillTimer);
|
|
1059
|
+
forceKillTimer = null;
|
|
1060
|
+
forceKillAt = Number.POSITIVE_INFINITY;
|
|
1061
|
+
};
|
|
1062
|
+
const scheduleForceKill = (delayMs) => {
|
|
1063
|
+
const boundedDelayMs = Math.max(1, delayMs);
|
|
1064
|
+
const killAt = Date.now() + boundedDelayMs;
|
|
1065
|
+
if (forceKillTimer && forceKillAt <= killAt)
|
|
1066
|
+
return;
|
|
1067
|
+
clearForceKillTimer();
|
|
1068
|
+
forceKillAt = killAt;
|
|
1069
|
+
forceKillTimer = setTimeout(() => {
|
|
1070
|
+
forceKillTimer = null;
|
|
1071
|
+
forceKillAt = Number.POSITIVE_INFINITY;
|
|
1072
|
+
if (isChildProcessTreeAlive(child)) {
|
|
1073
|
+
killChildProcessTree(child, true);
|
|
1074
|
+
}
|
|
1075
|
+
}, boundedDelayMs);
|
|
1076
|
+
};
|
|
800
1077
|
const timeout = opts.timeoutSec > 0
|
|
801
1078
|
? setTimeout(() => {
|
|
802
1079
|
timedOut = true;
|
|
803
1080
|
killChildProcessTree(child, false);
|
|
804
|
-
|
|
805
|
-
if (isChildProcessAlive(child)) {
|
|
806
|
-
killChildProcessTree(child, true);
|
|
807
|
-
}
|
|
808
|
-
}, Math.max(1, opts.graceSec) * 1000);
|
|
1081
|
+
scheduleForceKill(Math.max(1, opts.graceSec) * 1000);
|
|
809
1082
|
}, opts.timeoutSec * 1000)
|
|
810
1083
|
: null;
|
|
811
1084
|
let abortCleanup = null;
|
|
812
1085
|
if (opts.abortSignal) {
|
|
813
1086
|
const onAbort = () => {
|
|
814
1087
|
aborted = true;
|
|
1088
|
+
const operatorHardDeadlineMs = operatorInterruptHardDeadlineMs(opts.abortSignal?.reason);
|
|
1089
|
+
operatorInterrupted = operatorHardDeadlineMs !== null;
|
|
815
1090
|
killChildProcessTree(child, false);
|
|
816
|
-
|
|
817
|
-
if (isChildProcessAlive(child)) {
|
|
818
|
-
killChildProcessTree(child, true);
|
|
819
|
-
}
|
|
820
|
-
}, Math.max(1, opts.graceSec) * 1000);
|
|
1091
|
+
scheduleForceKill(operatorHardDeadlineMs ?? Math.max(1, opts.graceSec) * 1000);
|
|
821
1092
|
};
|
|
822
|
-
opts.abortSignal.
|
|
823
|
-
|
|
1093
|
+
if (opts.abortSignal.aborted) {
|
|
1094
|
+
onAbort();
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1098
|
+
abortCleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
1099
|
+
}
|
|
824
1100
|
}
|
|
825
1101
|
child.stdout?.on("data", (chunk) => {
|
|
826
1102
|
const text = String(chunk);
|
|
827
1103
|
stdout = appendWithCap(stdout, text);
|
|
1104
|
+
if (operatorInterrupted)
|
|
1105
|
+
return;
|
|
828
1106
|
logChain = logChain
|
|
829
|
-
.then(() => opts.onLog("stdout", text))
|
|
1107
|
+
.then(() => operatorInterrupted ? undefined : opts.onLog("stdout", text))
|
|
830
1108
|
.catch((err) => onLogError(err, runId, "failed to append stdout log chunk"));
|
|
831
1109
|
});
|
|
832
1110
|
child.stderr?.on("data", (chunk) => {
|
|
833
1111
|
const text = String(chunk);
|
|
834
1112
|
stderr = appendWithCap(stderr, text);
|
|
1113
|
+
if (operatorInterrupted)
|
|
1114
|
+
return;
|
|
835
1115
|
logChain = logChain
|
|
836
|
-
.then(() => opts.onLog("stderr", text))
|
|
1116
|
+
.then(() => operatorInterrupted ? undefined : opts.onLog("stderr", text))
|
|
837
1117
|
.catch((err) => onLogError(err, runId, "failed to append stderr log chunk"));
|
|
838
1118
|
});
|
|
839
1119
|
child.on("error", (err) => {
|
|
@@ -841,6 +1121,7 @@ export async function runChildProcess(runId, command, args, opts) {
|
|
|
841
1121
|
clearTimeout(timeout);
|
|
842
1122
|
if (abortCleanup)
|
|
843
1123
|
abortCleanup();
|
|
1124
|
+
clearForceKillTimer();
|
|
844
1125
|
runningProcesses.delete(runId);
|
|
845
1126
|
const errno = err.code;
|
|
846
1127
|
const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? "";
|
|
@@ -854,8 +1135,11 @@ export async function runChildProcess(runId, command, args, opts) {
|
|
|
854
1135
|
clearTimeout(timeout);
|
|
855
1136
|
if (abortCleanup)
|
|
856
1137
|
abortCleanup();
|
|
1138
|
+
if (!isChildProcessTreeAlive(child))
|
|
1139
|
+
clearForceKillTimer();
|
|
857
1140
|
runningProcesses.delete(runId);
|
|
858
|
-
|
|
1141
|
+
const completionBarrier = operatorInterrupted ? Promise.resolve() : logChain;
|
|
1142
|
+
void completionBarrier.finally(() => {
|
|
859
1143
|
resolve({
|
|
860
1144
|
exitCode: code,
|
|
861
1145
|
signal: aborted ? "SIGTERM" : signal,
|