@rudderhq/agent-runtime-utils 0.2.5-canary.9 → 0.2.5
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/server-utils.cli.d.ts +95 -0
- package/dist/server-utils.cli.d.ts.map +1 -0
- package/dist/server-utils.cli.js +788 -0
- package/dist/server-utils.cli.js.map +1 -0
- package/dist/server-utils.d.ts +4 -219
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.instructions.d.ts +41 -0
- package/dist/server-utils.instructions.d.ts.map +1 -0
- package/dist/server-utils.instructions.js +252 -0
- package/dist/server-utils.instructions.js.map +1 -0
- package/dist/server-utils.js +4 -1549
- package/dist/server-utils.js.map +1 -1
- package/dist/server-utils.process.d.ts +122 -0
- package/dist/server-utils.process.d.ts.map +1 -0
- package/dist/server-utils.process.js +249 -0
- package/dist/server-utils.process.js.map +1 -0
- package/dist/server-utils.prompts.d.ts +53 -0
- package/dist/server-utils.prompts.d.ts.map +1 -0
- package/dist/server-utils.prompts.js +271 -0
- package/dist/server-utils.prompts.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +14 -4
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { runningProcesses, isChildProcessAlive, RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES, DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES, DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS, isMaintainerOnlySkillTarget, skillLocationLabel, buildManagedSkillOrigin, compactSkillText, readSkillMetadataFromDirectory, resolveInstalledEntryTarget, parseObject, asString, appendWithCap } from "./server-utils.process.js";
|
|
7
|
+
import { defaultPathForPlatform, fileExists, resolveCommandPath, quoteForCmd, resolveSpawnTarget } from "./server-utils.instructions.js";
|
|
8
|
+
export function ensurePathInEnv(env) {
|
|
9
|
+
if (typeof env.PATH === "string" && env.PATH.length > 0)
|
|
10
|
+
return env;
|
|
11
|
+
if (typeof env.Path === "string" && env.Path.length > 0)
|
|
12
|
+
return env;
|
|
13
|
+
return { ...env, PATH: defaultPathForPlatform() };
|
|
14
|
+
}
|
|
15
|
+
export function prependPathEntry(env, entry) {
|
|
16
|
+
const normalized = ensurePathInEnv(env);
|
|
17
|
+
const pathKey = typeof normalized.PATH === "string" ? "PATH" : "Path";
|
|
18
|
+
const current = normalized[pathKey] ?? "";
|
|
19
|
+
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
20
|
+
const segments = current.split(delimiter).filter(Boolean);
|
|
21
|
+
if (segments.includes(entry))
|
|
22
|
+
return normalized;
|
|
23
|
+
return {
|
|
24
|
+
...normalized,
|
|
25
|
+
[pathKey]: current.length > 0 ? `${entry}${delimiter}${current}` : entry,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function findAncestorWithFile(startDir, relativePath, maxDepth = 12) {
|
|
29
|
+
let current = path.resolve(startDir);
|
|
30
|
+
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
31
|
+
const candidate = path.join(current, relativePath);
|
|
32
|
+
if (await fileExists(candidate))
|
|
33
|
+
return candidate;
|
|
34
|
+
const parent = path.dirname(current);
|
|
35
|
+
if (parent === current)
|
|
36
|
+
break;
|
|
37
|
+
current = parent;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function shellQuote(arg) {
|
|
42
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
43
|
+
}
|
|
44
|
+
export async function resolveRudderCliShimTarget(moduleDir) {
|
|
45
|
+
const packagedCli = await findAncestorWithFile(moduleDir, "desktop-cli.js");
|
|
46
|
+
if (packagedCli) {
|
|
47
|
+
return {
|
|
48
|
+
command: process.execPath,
|
|
49
|
+
args: [packagedCli],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const repoRoot = await findAncestorWithFile(moduleDir, path.join("cli", "src", "index.ts"));
|
|
53
|
+
if (!repoRoot)
|
|
54
|
+
return null;
|
|
55
|
+
const rootDir = path.dirname(path.dirname(path.dirname(repoRoot)));
|
|
56
|
+
const tsxEntry = path.join(rootDir, "cli", "node_modules", "tsx", "dist", "cli.mjs");
|
|
57
|
+
const cliSource = path.join(rootDir, "cli", "src", "index.ts");
|
|
58
|
+
if (await fileExists(tsxEntry)) {
|
|
59
|
+
return {
|
|
60
|
+
command: process.execPath,
|
|
61
|
+
args: [tsxEntry, cliSource],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const builtCliEntry = path.join(rootDir, "cli", "dist", "index.js");
|
|
65
|
+
if (await fileExists(builtCliEntry)) {
|
|
66
|
+
return {
|
|
67
|
+
command: process.execPath,
|
|
68
|
+
args: [builtCliEntry],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
export async function materializeRudderCliShim(target) {
|
|
74
|
+
const hash = createHash("sha1")
|
|
75
|
+
.update(JSON.stringify({ command: target.command, args: target.args, platform: process.platform }))
|
|
76
|
+
.digest("hex")
|
|
77
|
+
.slice(0, 12);
|
|
78
|
+
const shimDir = path.join(os.tmpdir(), "rudder-cli-shims", hash);
|
|
79
|
+
await fs.mkdir(shimDir, { recursive: true });
|
|
80
|
+
if (process.platform === "win32") {
|
|
81
|
+
const shimPath = path.join(shimDir, "rudder.cmd");
|
|
82
|
+
const commandLine = [quoteForCmd(target.command), ...target.args.map(quoteForCmd), "%*"].join(" ");
|
|
83
|
+
await fs.writeFile(shimPath, `@echo off\r\n${commandLine}\r\n`, "utf8");
|
|
84
|
+
return shimPath;
|
|
85
|
+
}
|
|
86
|
+
const shimPath = path.join(shimDir, "rudder");
|
|
87
|
+
const commandLine = [target.command, ...target.args].map(shellQuote).join(" ");
|
|
88
|
+
await fs.writeFile(shimPath, `#!/bin/sh\nexec ${commandLine} "$@"\n`, "utf8");
|
|
89
|
+
await fs.chmod(shimPath, 0o755);
|
|
90
|
+
return shimPath;
|
|
91
|
+
}
|
|
92
|
+
export async function ensureRudderCliInPath(moduleDir, env) {
|
|
93
|
+
const normalized = ensurePathInEnv(env);
|
|
94
|
+
const target = await resolveRudderCliShimTarget(moduleDir);
|
|
95
|
+
if (!target) {
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
const shimPath = await materializeRudderCliShim(target);
|
|
99
|
+
return prependPathEntry(normalized, path.dirname(shimPath));
|
|
100
|
+
}
|
|
101
|
+
export async function ensureAbsoluteDirectory(cwd, opts = {}) {
|
|
102
|
+
if (!path.isAbsolute(cwd)) {
|
|
103
|
+
throw new Error(`Working directory must be an absolute path: "${cwd}"`);
|
|
104
|
+
}
|
|
105
|
+
const assertDirectory = async () => {
|
|
106
|
+
const stats = await fs.stat(cwd);
|
|
107
|
+
if (!stats.isDirectory()) {
|
|
108
|
+
throw new Error(`Working directory is not a directory: "${cwd}"`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
await assertDirectory();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const code = err.code;
|
|
117
|
+
if (!opts.createIfMissing || code !== "ENOENT") {
|
|
118
|
+
if (code === "ENOENT") {
|
|
119
|
+
throw new Error(`Working directory does not exist: "${cwd}"`);
|
|
120
|
+
}
|
|
121
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
await fs.mkdir(cwd, { recursive: true });
|
|
126
|
+
await assertDirectory();
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
130
|
+
throw new Error(`Could not create working directory "${cwd}": ${reason}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function resolveRudderSkillsDir(moduleDir, additionalCandidates = []) {
|
|
134
|
+
const candidates = [
|
|
135
|
+
...RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES.map((relativePath) => path.resolve(moduleDir, relativePath)),
|
|
136
|
+
...additionalCandidates.map((candidate) => path.resolve(candidate)),
|
|
137
|
+
];
|
|
138
|
+
const seenRoots = new Set();
|
|
139
|
+
for (const root of candidates) {
|
|
140
|
+
if (seenRoots.has(root))
|
|
141
|
+
continue;
|
|
142
|
+
seenRoots.add(root);
|
|
143
|
+
const isDirectory = await fs.stat(root).then((stats) => stats.isDirectory()).catch(() => false);
|
|
144
|
+
if (isDirectory)
|
|
145
|
+
return root;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
export async function listRudderSkillEntries(moduleDir, additionalCandidates = []) {
|
|
150
|
+
const root = await resolveRudderSkillsDir(moduleDir, additionalCandidates);
|
|
151
|
+
if (!root)
|
|
152
|
+
return [];
|
|
153
|
+
try {
|
|
154
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
155
|
+
const skillDirectories = entries
|
|
156
|
+
.filter((entry) => entry.isDirectory())
|
|
157
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
158
|
+
const skillEntries = await Promise.all(skillDirectories.map(async (entry) => {
|
|
159
|
+
const source = path.join(root, entry.name);
|
|
160
|
+
const metadata = await readSkillMetadataFromDirectory(source);
|
|
161
|
+
return {
|
|
162
|
+
key: `rudder/${entry.name}`,
|
|
163
|
+
runtimeName: entry.name,
|
|
164
|
+
source,
|
|
165
|
+
name: metadata.name ?? entry.name,
|
|
166
|
+
description: metadata.description,
|
|
167
|
+
};
|
|
168
|
+
}));
|
|
169
|
+
return skillEntries;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function readInstalledSkillTargets(skillsHome) {
|
|
176
|
+
const entries = await fs.readdir(skillsHome, { withFileTypes: true }).catch(() => []);
|
|
177
|
+
const out = new Map();
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const fullPath = path.join(skillsHome, entry.name);
|
|
180
|
+
const linkedPath = entry.isSymbolicLink() ? await fs.readlink(fullPath).catch(() => null) : null;
|
|
181
|
+
out.set(entry.name, resolveInstalledEntryTarget(skillsHome, entry.name, entry, linkedPath));
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
export function buildPersistentSkillSnapshot(options) {
|
|
186
|
+
const { agentRuntimeType, availableEntries, desiredSkills, installed, skillsHome, locationLabel, installedDetail, missingDetail, externalConflictDetail, externalDetail, } = options;
|
|
187
|
+
const availableByKey = new Map(availableEntries.map((entry) => [entry.key, entry]));
|
|
188
|
+
const desiredSet = new Set(desiredSkills);
|
|
189
|
+
const entries = [];
|
|
190
|
+
const warnings = [...(options.warnings ?? [])];
|
|
191
|
+
for (const available of availableEntries) {
|
|
192
|
+
const installedEntry = installed.get(available.runtimeName) ?? null;
|
|
193
|
+
const desired = desiredSet.has(available.key);
|
|
194
|
+
let state = "available";
|
|
195
|
+
let managed = false;
|
|
196
|
+
let detail = null;
|
|
197
|
+
if (installedEntry?.targetPath === available.source) {
|
|
198
|
+
managed = true;
|
|
199
|
+
state = desired ? "installed" : "stale";
|
|
200
|
+
detail = installedDetail ?? null;
|
|
201
|
+
}
|
|
202
|
+
else if (installedEntry) {
|
|
203
|
+
state = "external";
|
|
204
|
+
detail = desired ? externalConflictDetail : externalDetail;
|
|
205
|
+
}
|
|
206
|
+
else if (desired) {
|
|
207
|
+
state = "missing";
|
|
208
|
+
detail = missingDetail;
|
|
209
|
+
}
|
|
210
|
+
entries.push({
|
|
211
|
+
key: available.key,
|
|
212
|
+
runtimeName: available.runtimeName,
|
|
213
|
+
description: available.description ?? null,
|
|
214
|
+
desired,
|
|
215
|
+
managed,
|
|
216
|
+
state,
|
|
217
|
+
sourcePath: available.source,
|
|
218
|
+
targetPath: path.join(skillsHome, available.runtimeName),
|
|
219
|
+
detail,
|
|
220
|
+
...buildManagedSkillOrigin(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
for (const desiredSkill of desiredSkills) {
|
|
224
|
+
if (availableByKey.has(desiredSkill))
|
|
225
|
+
continue;
|
|
226
|
+
warnings.push(`Desired skill "${desiredSkill}" is not available from the Rudder skills directory.`);
|
|
227
|
+
entries.push({
|
|
228
|
+
key: desiredSkill,
|
|
229
|
+
runtimeName: null,
|
|
230
|
+
desired: true,
|
|
231
|
+
managed: true,
|
|
232
|
+
state: "missing",
|
|
233
|
+
sourcePath: null,
|
|
234
|
+
targetPath: null,
|
|
235
|
+
detail: "Rudder cannot find this skill in the local runtime skills directory.",
|
|
236
|
+
origin: "external_unknown",
|
|
237
|
+
originLabel: "External or unavailable",
|
|
238
|
+
readOnly: false,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
for (const [name, installedEntry] of installed.entries()) {
|
|
242
|
+
if (availableEntries.some((entry) => entry.runtimeName === name))
|
|
243
|
+
continue;
|
|
244
|
+
entries.push({
|
|
245
|
+
key: name,
|
|
246
|
+
runtimeName: name,
|
|
247
|
+
description: null,
|
|
248
|
+
desired: false,
|
|
249
|
+
managed: false,
|
|
250
|
+
state: "external",
|
|
251
|
+
origin: "user_installed",
|
|
252
|
+
originLabel: "User-installed",
|
|
253
|
+
locationLabel: skillLocationLabel(locationLabel),
|
|
254
|
+
readOnly: true,
|
|
255
|
+
sourcePath: null,
|
|
256
|
+
targetPath: installedEntry.targetPath ?? path.join(skillsHome, name),
|
|
257
|
+
detail: externalDetail,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
261
|
+
return {
|
|
262
|
+
agentRuntimeType,
|
|
263
|
+
supported: true,
|
|
264
|
+
mode: "persistent",
|
|
265
|
+
desiredSkills,
|
|
266
|
+
entries,
|
|
267
|
+
warnings,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export function normalizeConfiguredPaperclipRuntimeSkills(value) {
|
|
271
|
+
if (!Array.isArray(value))
|
|
272
|
+
return [];
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const rawEntry of value) {
|
|
275
|
+
const entry = parseObject(rawEntry);
|
|
276
|
+
const key = asString(entry.key, asString(entry.name, "")).trim();
|
|
277
|
+
const runtimeName = asString(entry.runtimeName, asString(entry.name, "")).trim();
|
|
278
|
+
const source = asString(entry.source, "").trim();
|
|
279
|
+
if (!key || !runtimeName || !source)
|
|
280
|
+
continue;
|
|
281
|
+
out.push({
|
|
282
|
+
key,
|
|
283
|
+
runtimeName,
|
|
284
|
+
source,
|
|
285
|
+
name: compactSkillText(asString(entry.displayName, asString(entry.name, ""))) ?? runtimeName,
|
|
286
|
+
description: compactSkillText(typeof entry.description === "string"
|
|
287
|
+
? entry.description
|
|
288
|
+
: typeof entry.summary === "string"
|
|
289
|
+
? entry.summary
|
|
290
|
+
: null),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
export async function readRudderRuntimeSkillEntries(config, moduleDir, additionalCandidates = []) {
|
|
296
|
+
const configuredEntries = normalizeConfiguredPaperclipRuntimeSkills(config.rudderRuntimeSkills ?? config.paperclipRuntimeSkills);
|
|
297
|
+
if (configuredEntries.length > 0)
|
|
298
|
+
return configuredEntries;
|
|
299
|
+
return listRudderSkillEntries(moduleDir, additionalCandidates);
|
|
300
|
+
}
|
|
301
|
+
export async function readRudderSkillMarkdown(moduleDir, skillKey) {
|
|
302
|
+
const normalized = skillKey.trim().toLowerCase().replace(/^rudder\/rudder\//, "rudder/");
|
|
303
|
+
if (!normalized)
|
|
304
|
+
return null;
|
|
305
|
+
const entries = await listRudderSkillEntries(moduleDir);
|
|
306
|
+
const match = entries.find((entry) => entry.key === normalized);
|
|
307
|
+
if (!match)
|
|
308
|
+
return null;
|
|
309
|
+
try {
|
|
310
|
+
return await fs.readFile(path.join(match.source, "SKILL.md"), "utf8");
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export function readRudderSkillSyncPreference(config) {
|
|
317
|
+
const raw = config.rudderSkillSync ?? config.paperclipSkillSync;
|
|
318
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
319
|
+
return { explicit: false, desiredSkills: [] };
|
|
320
|
+
}
|
|
321
|
+
const syncConfig = raw;
|
|
322
|
+
const desiredValues = syncConfig.desiredSkills;
|
|
323
|
+
const desired = Array.isArray(desiredValues)
|
|
324
|
+
? desiredValues
|
|
325
|
+
.filter((value) => typeof value === "string")
|
|
326
|
+
.map((value) => value.trim())
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
: [];
|
|
329
|
+
return {
|
|
330
|
+
explicit: Object.prototype.hasOwnProperty.call(raw, "desiredSkills"),
|
|
331
|
+
desiredSkills: Array.from(new Set(desired)),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export function canonicalizeDesiredRudderSkillReference(reference, availableEntries) {
|
|
335
|
+
const normalizedReference = reference.trim().toLowerCase().replace(/^rudder\/rudder\//, "rudder/");
|
|
336
|
+
if (!normalizedReference)
|
|
337
|
+
return "";
|
|
338
|
+
const exactKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === normalizedReference);
|
|
339
|
+
if (exactKey)
|
|
340
|
+
return exactKey.key;
|
|
341
|
+
const byRuntimeName = availableEntries.filter((entry) => typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === normalizedReference);
|
|
342
|
+
if (byRuntimeName.length === 1)
|
|
343
|
+
return byRuntimeName[0].key;
|
|
344
|
+
const slugMatches = availableEntries.filter((entry) => entry.key.trim().toLowerCase().split("/").pop() === normalizedReference);
|
|
345
|
+
if (slugMatches.length === 1)
|
|
346
|
+
return slugMatches[0].key;
|
|
347
|
+
return normalizedReference;
|
|
348
|
+
}
|
|
349
|
+
export function resolveRudderDesiredSkillNames(config, availableEntries) {
|
|
350
|
+
const preference = readRudderSkillSyncPreference(config);
|
|
351
|
+
const desiredSkills = preference.desiredSkills
|
|
352
|
+
.map((reference) => canonicalizeDesiredRudderSkillReference(reference, availableEntries))
|
|
353
|
+
.filter(Boolean);
|
|
354
|
+
return Array.from(new Set(desiredSkills));
|
|
355
|
+
}
|
|
356
|
+
export function writeRudderSkillSyncPreference(config, desiredSkills) {
|
|
357
|
+
const next = { ...config };
|
|
358
|
+
const raw = next.rudderSkillSync;
|
|
359
|
+
const current = typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
360
|
+
? { ...raw }
|
|
361
|
+
: {};
|
|
362
|
+
current.desiredSkills = Array.from(new Set(desiredSkills
|
|
363
|
+
.map((value) => value.trim())
|
|
364
|
+
.filter(Boolean)));
|
|
365
|
+
next.rudderSkillSync = current;
|
|
366
|
+
return next;
|
|
367
|
+
}
|
|
368
|
+
export function nonEmptyEnvPath(value) {
|
|
369
|
+
return typeof value === "string" && value.trim().length > 0 ? path.resolve(value.trim()) : null;
|
|
370
|
+
}
|
|
371
|
+
export function resolveLocalOperatorHome(sourceEnv = process.env) {
|
|
372
|
+
return (nonEmptyEnvPath(sourceEnv.RUDDER_OPERATOR_HOME)
|
|
373
|
+
?? nonEmptyEnvPath(process.env.RUDDER_OPERATOR_HOME)
|
|
374
|
+
?? nonEmptyEnvPath(process.env.HOME)
|
|
375
|
+
?? nonEmptyEnvPath(sourceEnv.HOME)
|
|
376
|
+
?? path.resolve(os.homedir()));
|
|
377
|
+
}
|
|
378
|
+
export function applyLocalCliHomeEnv(targetEnv, sourceEnv = process.env) {
|
|
379
|
+
const home = nonEmptyEnvPath(sourceEnv.HOME) ?? path.resolve(os.homedir());
|
|
380
|
+
targetEnv.HOME = home;
|
|
381
|
+
const userProfile = nonEmptyEnvPath(sourceEnv.USERPROFILE);
|
|
382
|
+
if (userProfile) {
|
|
383
|
+
targetEnv.USERPROFILE = userProfile;
|
|
384
|
+
}
|
|
385
|
+
else if (process.platform === "win32") {
|
|
386
|
+
targetEnv.USERPROFILE = home;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
export async function localCliPathExists(candidate) {
|
|
390
|
+
return fs.access(candidate).then(() => true).catch(() => false);
|
|
391
|
+
}
|
|
392
|
+
export async function directoryIsEmpty(target) {
|
|
393
|
+
const entries = await fs.readdir(target).catch(() => null);
|
|
394
|
+
return Array.isArray(entries) && entries.length === 0;
|
|
395
|
+
}
|
|
396
|
+
export async function ensureSymlinkToSource(target, source) {
|
|
397
|
+
const existing = await fs.lstat(target).catch(() => null);
|
|
398
|
+
if (!existing) {
|
|
399
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
400
|
+
await fs.symlink(source, target);
|
|
401
|
+
return "created";
|
|
402
|
+
}
|
|
403
|
+
if (!existing.isSymbolicLink()) {
|
|
404
|
+
if (existing.isDirectory() && await directoryIsEmpty(target)) {
|
|
405
|
+
await fs.rmdir(target);
|
|
406
|
+
await fs.symlink(source, target);
|
|
407
|
+
return "repaired";
|
|
408
|
+
}
|
|
409
|
+
return "skipped";
|
|
410
|
+
}
|
|
411
|
+
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
412
|
+
if (!linkedPath)
|
|
413
|
+
return "skipped";
|
|
414
|
+
const resolvedLinkedPath = path.isAbsolute(linkedPath)
|
|
415
|
+
? linkedPath
|
|
416
|
+
: path.resolve(path.dirname(target), linkedPath);
|
|
417
|
+
if (resolvedLinkedPath === source)
|
|
418
|
+
return "skipped";
|
|
419
|
+
await fs.unlink(target);
|
|
420
|
+
await fs.symlink(source, target);
|
|
421
|
+
return "repaired";
|
|
422
|
+
}
|
|
423
|
+
export async function syncLocalCliCredentialHomeEntries(input) {
|
|
424
|
+
const sourceHome = nonEmptyEnvPath(input.sourceHome ?? undefined) ?? path.resolve(os.homedir());
|
|
425
|
+
const targetHome = path.resolve(input.targetHome);
|
|
426
|
+
const linked = [];
|
|
427
|
+
const skipped = [];
|
|
428
|
+
if (sourceHome === targetHome)
|
|
429
|
+
return { linked, skipped };
|
|
430
|
+
const entries = input.entries ?? DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES;
|
|
431
|
+
for (const relativeEntry of entries) {
|
|
432
|
+
const source = path.join(sourceHome, relativeEntry);
|
|
433
|
+
if (!(await localCliPathExists(source)))
|
|
434
|
+
continue;
|
|
435
|
+
const target = path.join(targetHome, relativeEntry);
|
|
436
|
+
try {
|
|
437
|
+
const result = await ensureSymlinkToSource(target, source);
|
|
438
|
+
if (result === "skipped")
|
|
439
|
+
skipped.push(relativeEntry);
|
|
440
|
+
else
|
|
441
|
+
linked.push(relativeEntry);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
skipped.push(relativeEntry);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (input.onLog && linked.length > 0) {
|
|
448
|
+
await input.onLog("stdout", `[rudder] Shared ${linked.length} local CLI credential entr${linked.length === 1 ? "y" : "ies"} into managed HOME ${targetHome}: ${linked.join(", ")}\n`);
|
|
449
|
+
}
|
|
450
|
+
return { linked, skipped };
|
|
451
|
+
}
|
|
452
|
+
export async function writeOperatorHomeShim(input) {
|
|
453
|
+
await fs.mkdir(input.shimDir, { recursive: true });
|
|
454
|
+
if (process.platform === "win32") {
|
|
455
|
+
const shimPath = path.join(input.shimDir, `${input.command}.cmd`);
|
|
456
|
+
const lines = [
|
|
457
|
+
"@echo off",
|
|
458
|
+
`set "HOME=${input.operatorHome}"`,
|
|
459
|
+
`set "USERPROFILE=${input.operatorHome}"`,
|
|
460
|
+
`${quoteForCmd(input.targetCommand)} %*`,
|
|
461
|
+
"",
|
|
462
|
+
];
|
|
463
|
+
await fs.writeFile(shimPath, lines.join("\r\n"), "utf8");
|
|
464
|
+
return shimPath;
|
|
465
|
+
}
|
|
466
|
+
const shimPath = path.join(input.shimDir, input.command);
|
|
467
|
+
await fs.writeFile(shimPath, [
|
|
468
|
+
"#!/bin/sh",
|
|
469
|
+
`export HOME=${shellQuote(input.operatorHome)}`,
|
|
470
|
+
`export USERPROFILE=${shellQuote(input.operatorHome)}`,
|
|
471
|
+
`exec ${shellQuote(input.targetCommand)} "$@"`,
|
|
472
|
+
"",
|
|
473
|
+
].join("\n"), "utf8");
|
|
474
|
+
await fs.chmod(shimPath, 0o755);
|
|
475
|
+
return shimPath;
|
|
476
|
+
}
|
|
477
|
+
export function normalizeShimCommand(input) {
|
|
478
|
+
return typeof input === "string" ? { command: input } : input;
|
|
479
|
+
}
|
|
480
|
+
export async function runCredentialShimAuthCheck(input) {
|
|
481
|
+
const env = {
|
|
482
|
+
...input.env,
|
|
483
|
+
HOME: input.home,
|
|
484
|
+
USERPROFILE: input.home,
|
|
485
|
+
};
|
|
486
|
+
return await new Promise((resolve) => {
|
|
487
|
+
const child = spawn(input.targetCommand, [...input.args], {
|
|
488
|
+
cwd: input.cwd,
|
|
489
|
+
env,
|
|
490
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
491
|
+
});
|
|
492
|
+
const timeout = setTimeout(() => {
|
|
493
|
+
child.kill("SIGTERM");
|
|
494
|
+
resolve(false);
|
|
495
|
+
}, 1000);
|
|
496
|
+
child.on("error", () => {
|
|
497
|
+
clearTimeout(timeout);
|
|
498
|
+
resolve(false);
|
|
499
|
+
});
|
|
500
|
+
child.on("close", (code) => {
|
|
501
|
+
clearTimeout(timeout);
|
|
502
|
+
resolve(code === 0);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
export async function shouldPrepareOperatorHomeShim(input) {
|
|
507
|
+
const authCheckArgs = input.command.authCheckArgs;
|
|
508
|
+
if (!authCheckArgs || authCheckArgs.length === 0)
|
|
509
|
+
return true;
|
|
510
|
+
if (input.command.credentialEntries && input.command.credentialEntries.length > 0) {
|
|
511
|
+
const hasOperatorCredentialEntry = await Promise.all(input.command.credentialEntries.map((entry) => localCliPathExists(path.join(input.operatorHome, entry))));
|
|
512
|
+
if (!hasOperatorCredentialEntry.some(Boolean))
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
const managedHomeWorks = await runCredentialShimAuthCheck({
|
|
516
|
+
targetCommand: input.targetCommand,
|
|
517
|
+
args: authCheckArgs,
|
|
518
|
+
cwd: input.cwd,
|
|
519
|
+
env: input.env,
|
|
520
|
+
home: input.targetHome,
|
|
521
|
+
});
|
|
522
|
+
if (managedHomeWorks)
|
|
523
|
+
return false;
|
|
524
|
+
return await runCredentialShimAuthCheck({
|
|
525
|
+
targetCommand: input.targetCommand,
|
|
526
|
+
args: authCheckArgs,
|
|
527
|
+
cwd: input.cwd,
|
|
528
|
+
env: input.env,
|
|
529
|
+
home: input.operatorHome,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
export async function ensureLocalCliCredentialShimsInPath(input) {
|
|
533
|
+
const operatorHome = nonEmptyEnvPath(input.operatorHome ?? undefined);
|
|
534
|
+
const targetHome = nonEmptyEnvPath(input.targetHome);
|
|
535
|
+
if (!operatorHome || !targetHome || operatorHome === targetHome) {
|
|
536
|
+
return ensurePathInEnv(input.env);
|
|
537
|
+
}
|
|
538
|
+
const normalized = ensurePathInEnv(input.env);
|
|
539
|
+
const cwd = input.cwd ?? process.cwd();
|
|
540
|
+
const commands = input.commands ?? DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS;
|
|
541
|
+
const shimDir = path.join(targetHome, ".rudder", "local-cli-shims");
|
|
542
|
+
const prepared = [];
|
|
543
|
+
for (const rawCommand of commands) {
|
|
544
|
+
const command = normalizeShimCommand(rawCommand);
|
|
545
|
+
const targetCommand = await resolveCommandPath(command.command, cwd, normalized);
|
|
546
|
+
if (!targetCommand)
|
|
547
|
+
continue;
|
|
548
|
+
if (path.dirname(targetCommand) === shimDir)
|
|
549
|
+
continue;
|
|
550
|
+
if (!(await shouldPrepareOperatorHomeShim({
|
|
551
|
+
command,
|
|
552
|
+
targetCommand,
|
|
553
|
+
cwd,
|
|
554
|
+
env: normalized,
|
|
555
|
+
targetHome,
|
|
556
|
+
operatorHome,
|
|
557
|
+
}))) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
await writeOperatorHomeShim({ shimDir, command: command.command, targetCommand, operatorHome });
|
|
561
|
+
prepared.push(command.command);
|
|
562
|
+
}
|
|
563
|
+
if (prepared.length === 0)
|
|
564
|
+
return normalized;
|
|
565
|
+
if (input.onLog) {
|
|
566
|
+
await input.onLog("stdout", `[rudder] Prepared local CLI credential shim${prepared.length === 1 ? "" : "s"} for: ${prepared.join(", ")}\n`);
|
|
567
|
+
}
|
|
568
|
+
return prependPathEntry(normalized, shimDir);
|
|
569
|
+
}
|
|
570
|
+
export async function ensureRudderSkillSymlink(source, target, linkSkill = (linkSource, linkTarget) => fs.symlink(linkSource, linkTarget)) {
|
|
571
|
+
const existing = await fs.lstat(target).catch(() => null);
|
|
572
|
+
if (!existing) {
|
|
573
|
+
await linkSkill(source, target);
|
|
574
|
+
return "created";
|
|
575
|
+
}
|
|
576
|
+
if (!existing.isSymbolicLink()) {
|
|
577
|
+
return "skipped";
|
|
578
|
+
}
|
|
579
|
+
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
580
|
+
if (!linkedPath)
|
|
581
|
+
return "skipped";
|
|
582
|
+
const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
|
|
583
|
+
if (resolvedLinkedPath === source) {
|
|
584
|
+
return "skipped";
|
|
585
|
+
}
|
|
586
|
+
const linkedPathExists = await fs.stat(resolvedLinkedPath).then(() => true).catch(() => false);
|
|
587
|
+
if (linkedPathExists) {
|
|
588
|
+
return "skipped";
|
|
589
|
+
}
|
|
590
|
+
await fs.unlink(target);
|
|
591
|
+
await linkSkill(source, target);
|
|
592
|
+
return "repaired";
|
|
593
|
+
}
|
|
594
|
+
export async function removeMaintainerOnlySkillSymlinks(skillsHome, allowedSkillNames) {
|
|
595
|
+
const allowed = new Set(Array.from(allowedSkillNames));
|
|
596
|
+
try {
|
|
597
|
+
const entries = await fs.readdir(skillsHome, { withFileTypes: true });
|
|
598
|
+
const removed = [];
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
if (allowed.has(entry.name))
|
|
601
|
+
continue;
|
|
602
|
+
const target = path.join(skillsHome, entry.name);
|
|
603
|
+
const existing = await fs.lstat(target).catch(() => null);
|
|
604
|
+
if (!existing?.isSymbolicLink())
|
|
605
|
+
continue;
|
|
606
|
+
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
607
|
+
if (!linkedPath)
|
|
608
|
+
continue;
|
|
609
|
+
const resolvedLinkedPath = path.isAbsolute(linkedPath)
|
|
610
|
+
? linkedPath
|
|
611
|
+
: path.resolve(path.dirname(target), linkedPath);
|
|
612
|
+
if (!isMaintainerOnlySkillTarget(linkedPath) &&
|
|
613
|
+
!isMaintainerOnlySkillTarget(resolvedLinkedPath)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
await fs.unlink(target);
|
|
617
|
+
removed.push(entry.name);
|
|
618
|
+
}
|
|
619
|
+
return removed;
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
export async function ensureCommandResolvable(command, cwd, env) {
|
|
626
|
+
const resolved = await resolveCommandPath(command, cwd, env);
|
|
627
|
+
if (resolved)
|
|
628
|
+
return;
|
|
629
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
630
|
+
const absolute = path.isAbsolute(command) ? command : path.resolve(cwd, command);
|
|
631
|
+
throw new Error(`Command is not executable: "${command}" (resolved: "${absolute}")`);
|
|
632
|
+
}
|
|
633
|
+
throw new Error(`Command not found in PATH: "${command}"`);
|
|
634
|
+
}
|
|
635
|
+
export async function runChildProcess(runId, command, args, opts) {
|
|
636
|
+
const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
|
|
637
|
+
return new Promise((resolve, reject) => {
|
|
638
|
+
const rawMerged = { ...process.env, ...opts.env };
|
|
639
|
+
const requestedHome = typeof opts.env.HOME === "string" && opts.env.HOME.trim().length > 0
|
|
640
|
+
? path.resolve(opts.env.HOME)
|
|
641
|
+
: null;
|
|
642
|
+
const inheritedHome = typeof process.env.HOME === "string" && process.env.HOME.trim().length > 0
|
|
643
|
+
? path.resolve(process.env.HOME)
|
|
644
|
+
: null;
|
|
645
|
+
const hasExplicitZdotdir = typeof opts.env.ZDOTDIR === "string" && opts.env.ZDOTDIR.trim().length > 0;
|
|
646
|
+
// Strip Claude Code nesting-guard env vars so spawned `claude` processes
|
|
647
|
+
// don't refuse to start with "cannot be launched inside another session".
|
|
648
|
+
// These vars leak in when the Rudder server itself is started from
|
|
649
|
+
// within a Claude Code session (e.g. `npx rudder run` in a terminal
|
|
650
|
+
// owned by Claude Code) or when cron inherits a contaminated shell env.
|
|
651
|
+
const CLAUDE_CODE_NESTING_VARS = [
|
|
652
|
+
"CLAUDECODE",
|
|
653
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
654
|
+
"CLAUDE_CODE_SESSION",
|
|
655
|
+
"CLAUDE_CODE_PARENT_SESSION",
|
|
656
|
+
];
|
|
657
|
+
for (const key of CLAUDE_CODE_NESTING_VARS) {
|
|
658
|
+
delete rawMerged[key];
|
|
659
|
+
}
|
|
660
|
+
const GIT_IDENTITY_ENV_VARS = [
|
|
661
|
+
"GIT_AUTHOR_NAME",
|
|
662
|
+
"GIT_AUTHOR_EMAIL",
|
|
663
|
+
"GIT_COMMITTER_NAME",
|
|
664
|
+
"GIT_COMMITTER_EMAIL",
|
|
665
|
+
];
|
|
666
|
+
for (const key of GIT_IDENTITY_ENV_VARS) {
|
|
667
|
+
if (rawMerged[key] === "" && !Object.prototype.hasOwnProperty.call(opts.env, key)) {
|
|
668
|
+
delete rawMerged[key];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// When Rudder isolates HOME for child agents, don't let zsh keep using the
|
|
672
|
+
// host user's startup dir via an inherited ZDOTDIR. That mismatch makes
|
|
673
|
+
// child `zsh -lc` invocations source the host `.zshenv` with the agent HOME.
|
|
674
|
+
if (requestedHome && requestedHome !== inheritedHome && !hasExplicitZdotdir) {
|
|
675
|
+
delete rawMerged.ZDOTDIR;
|
|
676
|
+
}
|
|
677
|
+
const mergedEnv = ensurePathInEnv(rawMerged);
|
|
678
|
+
void resolveSpawnTarget(command, args, opts.cwd, mergedEnv)
|
|
679
|
+
.then((target) => {
|
|
680
|
+
if (opts.abortSignal?.aborted) {
|
|
681
|
+
resolve({
|
|
682
|
+
exitCode: null,
|
|
683
|
+
signal: "SIGTERM",
|
|
684
|
+
timedOut: false,
|
|
685
|
+
stdout: "",
|
|
686
|
+
stderr: "",
|
|
687
|
+
pid: null,
|
|
688
|
+
startedAt: null,
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const child = spawn(target.command, target.args, {
|
|
693
|
+
cwd: opts.cwd,
|
|
694
|
+
env: mergedEnv,
|
|
695
|
+
shell: false,
|
|
696
|
+
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
697
|
+
});
|
|
698
|
+
const startedAt = new Date().toISOString();
|
|
699
|
+
if (opts.stdin != null && child.stdin) {
|
|
700
|
+
child.stdin.write(opts.stdin);
|
|
701
|
+
child.stdin.end();
|
|
702
|
+
}
|
|
703
|
+
if (typeof child.pid === "number" && child.pid > 0 && opts.onSpawn) {
|
|
704
|
+
void opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => {
|
|
705
|
+
onLogError(err, runId, "failed to record child process metadata");
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
runningProcesses.set(runId, { child, graceSec: opts.graceSec });
|
|
709
|
+
let timedOut = false;
|
|
710
|
+
let aborted = false;
|
|
711
|
+
let stdout = "";
|
|
712
|
+
let stderr = "";
|
|
713
|
+
let logChain = Promise.resolve();
|
|
714
|
+
const timeout = opts.timeoutSec > 0
|
|
715
|
+
? setTimeout(() => {
|
|
716
|
+
timedOut = true;
|
|
717
|
+
child.kill("SIGTERM");
|
|
718
|
+
setTimeout(() => {
|
|
719
|
+
if (isChildProcessAlive(child)) {
|
|
720
|
+
child.kill("SIGKILL");
|
|
721
|
+
}
|
|
722
|
+
}, Math.max(1, opts.graceSec) * 1000);
|
|
723
|
+
}, opts.timeoutSec * 1000)
|
|
724
|
+
: null;
|
|
725
|
+
let abortCleanup = null;
|
|
726
|
+
if (opts.abortSignal) {
|
|
727
|
+
const onAbort = () => {
|
|
728
|
+
aborted = true;
|
|
729
|
+
child.kill("SIGTERM");
|
|
730
|
+
setTimeout(() => {
|
|
731
|
+
if (isChildProcessAlive(child)) {
|
|
732
|
+
child.kill("SIGKILL");
|
|
733
|
+
}
|
|
734
|
+
}, Math.max(1, opts.graceSec) * 1000);
|
|
735
|
+
};
|
|
736
|
+
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
737
|
+
abortCleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
738
|
+
}
|
|
739
|
+
child.stdout?.on("data", (chunk) => {
|
|
740
|
+
const text = String(chunk);
|
|
741
|
+
stdout = appendWithCap(stdout, text);
|
|
742
|
+
logChain = logChain
|
|
743
|
+
.then(() => opts.onLog("stdout", text))
|
|
744
|
+
.catch((err) => onLogError(err, runId, "failed to append stdout log chunk"));
|
|
745
|
+
});
|
|
746
|
+
child.stderr?.on("data", (chunk) => {
|
|
747
|
+
const text = String(chunk);
|
|
748
|
+
stderr = appendWithCap(stderr, text);
|
|
749
|
+
logChain = logChain
|
|
750
|
+
.then(() => opts.onLog("stderr", text))
|
|
751
|
+
.catch((err) => onLogError(err, runId, "failed to append stderr log chunk"));
|
|
752
|
+
});
|
|
753
|
+
child.on("error", (err) => {
|
|
754
|
+
if (timeout)
|
|
755
|
+
clearTimeout(timeout);
|
|
756
|
+
if (abortCleanup)
|
|
757
|
+
abortCleanup();
|
|
758
|
+
runningProcesses.delete(runId);
|
|
759
|
+
const errno = err.code;
|
|
760
|
+
const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? "";
|
|
761
|
+
const msg = errno === "ENOENT"
|
|
762
|
+
? `Failed to start command "${command}" in "${opts.cwd}". Verify adapter command, working directory, and PATH (${pathValue}).`
|
|
763
|
+
: `Failed to start command "${command}" in "${opts.cwd}": ${err.message}`;
|
|
764
|
+
reject(new Error(msg));
|
|
765
|
+
});
|
|
766
|
+
child.on("close", (code, signal) => {
|
|
767
|
+
if (timeout)
|
|
768
|
+
clearTimeout(timeout);
|
|
769
|
+
if (abortCleanup)
|
|
770
|
+
abortCleanup();
|
|
771
|
+
runningProcesses.delete(runId);
|
|
772
|
+
void logChain.finally(() => {
|
|
773
|
+
resolve({
|
|
774
|
+
exitCode: code,
|
|
775
|
+
signal: aborted ? "SIGTERM" : signal,
|
|
776
|
+
timedOut,
|
|
777
|
+
stdout,
|
|
778
|
+
stderr,
|
|
779
|
+
pid: child.pid ?? null,
|
|
780
|
+
startedAt,
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
})
|
|
785
|
+
.catch(reject);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
//# sourceMappingURL=server-utils.cli.js.map
|