agent-cloud-kit 0.0.0-stage → 1.0.0-rc.1
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/LICENSE +21 -0
- package/README.md +232 -2
- package/dist/ca.js +30 -0
- package/dist/cli.js +418 -0
- package/dist/config.js +197 -0
- package/dist/constants.js +77 -0
- package/dist/detect.js +265 -0
- package/dist/docker.js +95 -0
- package/dist/doctor.js +200 -0
- package/dist/init.js +136 -0
- package/dist/installers.js +463 -0
- package/dist/launcher.js +140 -0
- package/dist/manual.js +223 -0
- package/dist/prepare/detect.js +29 -0
- package/dist/prepare/index.js +489 -0
- package/dist/prepare/prompts.js +28 -0
- package/dist/prepare/questions.js +5 -0
- package/dist/run.js +113 -0
- package/dist/session.js +246 -0
- package/dist/setup.js +277 -0
- package/dist/skill.js +148 -0
- package/dist/state.js +147 -0
- package/dist/steps.js +46 -0
- package/package.json +46 -4
- package/schema.json +72 -0
- package/skills/cloud-ready/SKILL.md +202 -0
- package/skills/cloud-ready/reference/compat.md +110 -0
- package/skills/cloud-ready/reference/knowledge.md +56 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tool installers whose download sources pass the Claude cloud egress proxy
|
|
3
|
+
* (nodejs.org, npm and PyPI; GitHub releases are blocked).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { TOOL_INSTALL_TIMEOUT_MS } from "./constants.js";
|
|
9
|
+
import { TOOLS } from "./detect.js";
|
|
10
|
+
import { step } from "./steps.js";
|
|
11
|
+
/** Base URL of the Node.js distribution. */
|
|
12
|
+
const NODE_DIST_URL = "https://nodejs.org/dist";
|
|
13
|
+
/** Node.js archive architectures by `process.arch`. */
|
|
14
|
+
const NODE_ARCHS = { x64: "x64", arm64: "arm64" };
|
|
15
|
+
/** Executable name checked for each tool to decide whether it is installed. */
|
|
16
|
+
const EXECUTABLES = {
|
|
17
|
+
node: "node",
|
|
18
|
+
bun: "bun",
|
|
19
|
+
deno: "deno",
|
|
20
|
+
pnpm: "pnpm",
|
|
21
|
+
yarn: "yarn",
|
|
22
|
+
uv: "uv",
|
|
23
|
+
};
|
|
24
|
+
/** npm package name for yarn 2 and later. */
|
|
25
|
+
const YARN_BERRY_PACKAGE = "@yarnpkg/cli-dist";
|
|
26
|
+
/** npm package that provides each npm-installed tool (yarn is resolved by version). */
|
|
27
|
+
const NPM_PACKAGES = {
|
|
28
|
+
bun: "bun",
|
|
29
|
+
deno: "deno",
|
|
30
|
+
pnpm: "pnpm",
|
|
31
|
+
};
|
|
32
|
+
/** Strict shape of an accepted version: no leading `-` or `/`, no shell metacharacters. */
|
|
33
|
+
const VERSION_PATTERN = /^\d+(\.\d+){0,2}([-+][0-9A-Za-z.]+)?$/;
|
|
34
|
+
/** An exact Node.js version (`x.y.z`, optionally with a suffix): installed as is, no lookup. */
|
|
35
|
+
const NODE_EXACT = /^\d+\.\d+\.\d+([-+][0-9A-Za-z.]+)?$/;
|
|
36
|
+
/** Node.js aliases resolved through the release index: `lts/<name>`, `lts/*` and `node`. */
|
|
37
|
+
const NODE_ALIAS = /^(lts\/(\*|[A-Za-z]+)|node)$/;
|
|
38
|
+
/** Alias for the newest Node.js release. */
|
|
39
|
+
const NODE_LATEST = "node";
|
|
40
|
+
/** Alias for the newest Node.js LTS release. */
|
|
41
|
+
const NODE_LTS_ANY = "lts/*";
|
|
42
|
+
/** Prefix of a named Node.js LTS alias. */
|
|
43
|
+
const NODE_LTS_PREFIX = "lts/";
|
|
44
|
+
/** Index of every Node.js release, newest first. */
|
|
45
|
+
const NODE_INDEX_URL = `${NODE_DIST_URL}/index.json`;
|
|
46
|
+
/** Time limit of the index download, in seconds. */
|
|
47
|
+
const NODE_INDEX_TIMEOUT_SECONDS = "60";
|
|
48
|
+
/** File name of the downloaded index inside its temp directory. */
|
|
49
|
+
const NODE_INDEX_FILE = "index.json";
|
|
50
|
+
/** File name of the downloaded checksum list inside its temp directory. */
|
|
51
|
+
const NODE_SHASUMS_FILE = "SHASUMS256.txt";
|
|
52
|
+
/** File name of the downloaded tarball inside its temp directory. */
|
|
53
|
+
const NODE_TARBALL_FILE = "node.tar.xz";
|
|
54
|
+
/** Prefix of the temp download directories of the node installer. */
|
|
55
|
+
const NODE_DOWNLOAD_PREFIX = "agent-cloud-kit-node-";
|
|
56
|
+
/** Suffix of a temp install directory (after the pid), renamed to the final one on success. */
|
|
57
|
+
const TEMP_SUFFIX = "tmp";
|
|
58
|
+
/** A line of SHASUMS256.txt: a 64-digit hex hash, two spaces (or ` *`) and the file name. */
|
|
59
|
+
const SHASUMS_LINE = /^([0-9a-f]{64}) [ *](.+)$/;
|
|
60
|
+
/**
|
|
61
|
+
* Script that downloads (`$1`) into a file (`$3`), checks it against the expected sha256 (`$4`)
|
|
62
|
+
* and extracts it into a directory (`$2`); fails if curl, the checksum or tar fails.
|
|
63
|
+
*/
|
|
64
|
+
const NODE_INSTALL_SCRIPT = 'set -eo pipefail; curl -fsSL -o "$3" -- "$1"; echo "$4 $3" | sha256sum -c -; tar -xJ -f "$3" -C "$2" --strip-components=1';
|
|
65
|
+
/** Placeholder for `$0` in the node install script. */
|
|
66
|
+
const SCRIPT_NAME = "_";
|
|
67
|
+
/**
|
|
68
|
+
* Finds the sha256 of a file in the text of a SHASUMS256.txt.
|
|
69
|
+
*
|
|
70
|
+
* @param text - Content of SHASUMS256.txt.
|
|
71
|
+
* @param file - File name, such as `node-v24.14.1-linux-x64.tar.xz`.
|
|
72
|
+
* @returns The lowercase hex hash, or undefined when the file is not listed.
|
|
73
|
+
* @example
|
|
74
|
+
* parseShasums(`${"a".repeat(64)} node-v24.14.1-linux-x64.tar.xz`, "node-v24.14.1-linux-x64.tar.xz"); // "aaaa..."
|
|
75
|
+
*/
|
|
76
|
+
export function parseShasums(text, file) {
|
|
77
|
+
for (const line of text.split("\n")) {
|
|
78
|
+
const match = SHASUMS_LINE.exec(line.trimEnd());
|
|
79
|
+
if (match && match[2] === file)
|
|
80
|
+
return match[1];
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Installs into `<target>.<pid>.tmp` and renames it to `target` only when `work` succeeds, so a
|
|
86
|
+
* half-extracted install never leaves `target` behind (an existing `target` would be skipped).
|
|
87
|
+
*
|
|
88
|
+
* @param target - Final install directory.
|
|
89
|
+
* @param work - Installs into the temp directory it receives.
|
|
90
|
+
* @returns Resolves when `target` is in place.
|
|
91
|
+
* @throws What `work` throws, after removing the temp directory.
|
|
92
|
+
* @example
|
|
93
|
+
* await installAtomically("/opt/agent-cloud-kit/uv-0.5.0", async (temp) => { await install(temp); });
|
|
94
|
+
*/
|
|
95
|
+
export async function installAtomically(target, work) {
|
|
96
|
+
const temp = `${target}.${process.pid}.${TEMP_SUFFIX}`;
|
|
97
|
+
rmSync(temp, { recursive: true, force: true });
|
|
98
|
+
mkdirSync(temp, { recursive: true });
|
|
99
|
+
try {
|
|
100
|
+
await work(temp);
|
|
101
|
+
rmSync(target, { recursive: true, force: true });
|
|
102
|
+
renameSync(temp, target);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
rmSync(temp, { recursive: true, force: true });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Downloads the checksum list of a Node.js release with curl and finds the hash of one archive.
|
|
111
|
+
*
|
|
112
|
+
* @param run - Process runner.
|
|
113
|
+
* @param env - Environment for curl.
|
|
114
|
+
* @param dir - Directory for the downloaded list.
|
|
115
|
+
* @param version - Exact Node.js version.
|
|
116
|
+
* @param file - Archive name to look up.
|
|
117
|
+
* @returns The expected sha256.
|
|
118
|
+
* @throws When the list does not name the archive.
|
|
119
|
+
*/
|
|
120
|
+
async function nodeExpectedHash(run, env, dir, version, file) {
|
|
121
|
+
const path = join(dir, NODE_SHASUMS_FILE);
|
|
122
|
+
const url = `${NODE_DIST_URL}/v${version}/${NODE_SHASUMS_FILE}`;
|
|
123
|
+
await run("curl", ["-fsSL", "-o", path, "--", url], { env, timeoutMs: TOOL_INSTALL_TIMEOUT_MS });
|
|
124
|
+
const hash = parseShasums(readFileSync(path, "utf8"), file);
|
|
125
|
+
if (!hash)
|
|
126
|
+
throw new Error(`${file} is not listed in ${NODE_SHASUMS_FILE}`);
|
|
127
|
+
return hash;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Maps a Node.js `process.arch` to the name used in its release archives.
|
|
131
|
+
*
|
|
132
|
+
* @param arch - Value of `process.arch`.
|
|
133
|
+
* @returns The archive architecture.
|
|
134
|
+
* @throws When the architecture has no Linux build we support.
|
|
135
|
+
* @example
|
|
136
|
+
* nodeArch("x64"); // "x64"
|
|
137
|
+
*/
|
|
138
|
+
export function nodeArch(arch) {
|
|
139
|
+
const mapped = NODE_ARCHS[arch];
|
|
140
|
+
if (!mapped)
|
|
141
|
+
throw new Error(`unsupported architecture for node: ${arch}`);
|
|
142
|
+
return mapped;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Checks that a version is safe to use in paths and command arguments.
|
|
146
|
+
*
|
|
147
|
+
* @param tool - Tool name, for the error message.
|
|
148
|
+
* @param version - Version taken from an untrusted file.
|
|
149
|
+
* @returns Nothing when valid.
|
|
150
|
+
* @throws When the version does not match the strict pattern.
|
|
151
|
+
* @example
|
|
152
|
+
* assertVersion("node", "24.14.1"); // ok
|
|
153
|
+
*/
|
|
154
|
+
export function assertVersion(tool, version) {
|
|
155
|
+
if (!VERSION_PATTERN.test(version))
|
|
156
|
+
throw new Error(`invalid version for ${tool}: ${version}`);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Tells whether a version is safe to use.
|
|
160
|
+
*
|
|
161
|
+
* @param version - Version taken from an untrusted file.
|
|
162
|
+
* @returns True when it matches the strict pattern.
|
|
163
|
+
*/
|
|
164
|
+
function isValidVersion(version) {
|
|
165
|
+
return VERSION_PATTERN.test(version);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Tells whether a requested version is accepted for a tool: the strict pattern, plus the
|
|
169
|
+
* `lts/<name>`, `lts/*` and `node` aliases for node only. Aliases never become path segments.
|
|
170
|
+
*
|
|
171
|
+
* @param tool - Tool name.
|
|
172
|
+
* @param version - Version taken from an untrusted file.
|
|
173
|
+
* @returns True when it can be installed (possibly after resolving it).
|
|
174
|
+
* @example
|
|
175
|
+
* isValidRequest("node", "lts/iron"); // true
|
|
176
|
+
*/
|
|
177
|
+
export function isValidRequest(tool, version) {
|
|
178
|
+
return isValidVersion(version) || (tool === "node" && NODE_ALIAS.test(version));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Compares two exact `x.y.z` versions numerically.
|
|
182
|
+
*
|
|
183
|
+
* @param a - First version.
|
|
184
|
+
* @param b - Second version.
|
|
185
|
+
* @returns A positive number when `a` is newer, negative when older, 0 when equal.
|
|
186
|
+
*/
|
|
187
|
+
function compareVersions(a, b) {
|
|
188
|
+
const pa = a.split(/[.+-]/).slice(0, 3).map(Number);
|
|
189
|
+
const pb = b.split(/[.+-]/).slice(0, 3).map(Number);
|
|
190
|
+
for (let i = 0; i < pa.length; i++) {
|
|
191
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
192
|
+
if (diff)
|
|
193
|
+
return diff;
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Tells whether an exact version satisfies a requested node version.
|
|
199
|
+
*
|
|
200
|
+
* @param requested - Version as written in the repo (`22`, `22.1`, `lts/iron`, `node`...).
|
|
201
|
+
* @param exact - Exact version without `v`.
|
|
202
|
+
* @param lts - LTS codename of the exact version, when known; `undefined` skips the alias check.
|
|
203
|
+
* @returns True when it matches the numeric prefix, or the alias.
|
|
204
|
+
* @example
|
|
205
|
+
* nodeMatches("22.1", "22.1.5"); // true
|
|
206
|
+
*/
|
|
207
|
+
export function nodeMatches(requested, exact, lts) {
|
|
208
|
+
if (NODE_ALIAS.test(requested)) {
|
|
209
|
+
if (lts === undefined || requested === NODE_LATEST)
|
|
210
|
+
return true;
|
|
211
|
+
if (requested === NODE_LTS_ANY)
|
|
212
|
+
return lts !== false;
|
|
213
|
+
return lts !== false && lts.toLowerCase() === requested.slice(NODE_LTS_PREFIX.length).toLowerCase();
|
|
214
|
+
}
|
|
215
|
+
const want = requested.split(".");
|
|
216
|
+
const have = exact.split(".");
|
|
217
|
+
return want.every((part, i) => part === have[i]);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Picks the newest release of the index that satisfies a requested node version.
|
|
221
|
+
*
|
|
222
|
+
* @param requested - Non-exact version (`22`, `22.1`, `lts/iron`, `lts/*` or `node`).
|
|
223
|
+
* @param index - Release index.
|
|
224
|
+
* @returns The exact version without `v`, or undefined when nothing matches.
|
|
225
|
+
* @example
|
|
226
|
+
* resolveNodeVersion("lts/iron", [{ version: "v20.18.1", lts: "Iron" }]); // "20.18.1"
|
|
227
|
+
*/
|
|
228
|
+
export function resolveNodeVersion(requested, index) {
|
|
229
|
+
let best;
|
|
230
|
+
for (const release of index) {
|
|
231
|
+
const exact = release.version.replace(/^v/, "");
|
|
232
|
+
if (!NODE_EXACT.test(exact) || !nodeMatches(requested, exact, release.lts))
|
|
233
|
+
continue;
|
|
234
|
+
if (!best || compareVersions(exact, best) > 0)
|
|
235
|
+
best = exact;
|
|
236
|
+
}
|
|
237
|
+
return best;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Builds a fetcher that downloads the Node.js release index with curl into a temp file. The URL
|
|
241
|
+
* and the file travel as argv, and curl honors the proxy environment.
|
|
242
|
+
*
|
|
243
|
+
* @param run - Process runner.
|
|
244
|
+
* @param env - Environment for curl.
|
|
245
|
+
* @returns The fetcher.
|
|
246
|
+
* @example
|
|
247
|
+
* const index = await curlNodeIndex(runProcess, process.env)();
|
|
248
|
+
*/
|
|
249
|
+
export function curlNodeIndex(run, env) {
|
|
250
|
+
return async () => {
|
|
251
|
+
const dir = mkdtempSync(join(tmpdir(), "agent-cloud-kit-node-index-"));
|
|
252
|
+
try {
|
|
253
|
+
const file = join(dir, NODE_INDEX_FILE);
|
|
254
|
+
await run("curl", ["-fsSL", "--max-time", NODE_INDEX_TIMEOUT_SECONDS, "-o", file, "--", NODE_INDEX_URL], { env });
|
|
255
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
256
|
+
if (!Array.isArray(parsed))
|
|
257
|
+
throw new Error("the node release index is not a list");
|
|
258
|
+
return parsed.filter((r) => typeof r === "object" && r !== null && typeof r.version === "string" && (r.lts === false || typeof r.lts === "string"));
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
rmSync(dir, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Turns the requested versions into installable ones: a non-exact node version is resolved to an
|
|
267
|
+
* exact one through the release index; every other tool is kept as requested.
|
|
268
|
+
*
|
|
269
|
+
* @param versions - Requested versions by tool.
|
|
270
|
+
* @param fetchIndex - Downloads the release index; called only when node needs resolving.
|
|
271
|
+
* @returns The installable versions (node left out when it cannot be resolved) and one failed
|
|
272
|
+
* result when the resolution failed.
|
|
273
|
+
* @example
|
|
274
|
+
* await resolveVersions({ node: "22" }, curlNodeIndex(runProcess, process.env)); // { resolved: { node: "22.12.0" }, results: [] }
|
|
275
|
+
*/
|
|
276
|
+
export async function resolveVersions(versions, fetchIndex) {
|
|
277
|
+
const resolved = { ...versions };
|
|
278
|
+
const requested = versions.node;
|
|
279
|
+
if (!requested || NODE_EXACT.test(requested) || !isValidRequest("node", requested))
|
|
280
|
+
return { resolved, results: [] };
|
|
281
|
+
delete resolved.node;
|
|
282
|
+
let exact;
|
|
283
|
+
const result = await step(`node ${requested}`, async () => {
|
|
284
|
+
exact = resolveNodeVersion(requested, await fetchIndex());
|
|
285
|
+
if (!exact)
|
|
286
|
+
throw new Error(`no node release matches ${requested}`);
|
|
287
|
+
});
|
|
288
|
+
if (!exact)
|
|
289
|
+
return { resolved, results: [result] };
|
|
290
|
+
resolved.node = exact;
|
|
291
|
+
return { resolved, results: [] };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Finds the version whose directory holds a tool: the requested one, or for a non-exact node
|
|
295
|
+
* version the exact one setup resolved it to.
|
|
296
|
+
*
|
|
297
|
+
* @param tool - Tool name.
|
|
298
|
+
* @param requested - Version as written in the repo.
|
|
299
|
+
* @param resolved - Versions setup installed (from the machine state).
|
|
300
|
+
* @returns The version used in the install path, or undefined when there is none.
|
|
301
|
+
*/
|
|
302
|
+
function installedVersion(tool, requested, resolved) {
|
|
303
|
+
if (tool !== "node")
|
|
304
|
+
return isValidVersion(requested) ? requested : undefined;
|
|
305
|
+
if (NODE_EXACT.test(requested))
|
|
306
|
+
return requested;
|
|
307
|
+
const exact = resolved.node;
|
|
308
|
+
if (!exact || !NODE_EXACT.test(exact) || !isValidRequest("node", requested))
|
|
309
|
+
return undefined;
|
|
310
|
+
return nodeMatches(requested, exact) ? exact : undefined;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Builds the default directory layout for a tool.
|
|
314
|
+
*
|
|
315
|
+
* @param tool - Tool name.
|
|
316
|
+
* @param binSubdir - Path of the bin directory inside the install directory.
|
|
317
|
+
* @returns The `dir` and `binDir` functions.
|
|
318
|
+
*/
|
|
319
|
+
function layout(tool, binSubdir) {
|
|
320
|
+
const dir = (toolsDir, version) => {
|
|
321
|
+
assertVersion(tool, version);
|
|
322
|
+
return join(toolsDir, `${tool}-${version}`);
|
|
323
|
+
};
|
|
324
|
+
return { dir, binDir: (toolsDir, version) => join(dir(toolsDir, version), binSubdir) };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Creates an installer that runs `npm install` into the tool's own prefix.
|
|
328
|
+
*
|
|
329
|
+
* @param tool - Tool name.
|
|
330
|
+
* @param packageFor - Resolves the npm package name for a version.
|
|
331
|
+
* @returns The installer.
|
|
332
|
+
*/
|
|
333
|
+
function npmInstaller(tool, packageFor) {
|
|
334
|
+
const base = layout(tool, join("node_modules", ".bin"));
|
|
335
|
+
return {
|
|
336
|
+
...base,
|
|
337
|
+
install: (toolsDir, version, run, env) => installAtomically(base.dir(toolsDir, version), (temp) => run("npm", ["install", "--prefix", temp, "--no-save", "--no-audit", "--no-fund", `${packageFor(version)}@${version}`], { env, timeoutMs: TOOL_INSTALL_TIMEOUT_MS })),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const nodeLayout = layout("node", "bin");
|
|
341
|
+
const uvLayout = layout("uv", "bin");
|
|
342
|
+
/** Installers by tool. */
|
|
343
|
+
export const INSTALLERS = {
|
|
344
|
+
node: {
|
|
345
|
+
...nodeLayout,
|
|
346
|
+
install: (toolsDir, version, run, env) => installAtomically(nodeLayout.dir(toolsDir, version), async (temp) => {
|
|
347
|
+
const file = `node-v${version}-linux-${nodeArch(process.arch)}.tar.xz`;
|
|
348
|
+
const downloads = mkdtempSync(join(tmpdir(), NODE_DOWNLOAD_PREFIX));
|
|
349
|
+
try {
|
|
350
|
+
const hash = await nodeExpectedHash(run, env, downloads, version, file);
|
|
351
|
+
const url = `${NODE_DIST_URL}/v${version}/${file}`;
|
|
352
|
+
await run("bash", ["-c", NODE_INSTALL_SCRIPT, SCRIPT_NAME, url, temp, join(downloads, NODE_TARBALL_FILE), hash], {
|
|
353
|
+
env,
|
|
354
|
+
timeoutMs: TOOL_INSTALL_TIMEOUT_MS,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
rmSync(downloads, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
}),
|
|
361
|
+
},
|
|
362
|
+
bun: npmInstaller("bun", () => NPM_PACKAGES.bun),
|
|
363
|
+
deno: npmInstaller("deno", () => NPM_PACKAGES.deno),
|
|
364
|
+
pnpm: npmInstaller("pnpm", () => NPM_PACKAGES.pnpm),
|
|
365
|
+
yarn: npmInstaller("yarn", (version) => (version.split(".")[0] === "1" ? "yarn" : YARN_BERRY_PACKAGE)),
|
|
366
|
+
uv: {
|
|
367
|
+
...uvLayout,
|
|
368
|
+
install: (toolsDir, version, run, env) => installAtomically(uvLayout.dir(toolsDir, version), (temp) => run("python3", ["-m", "pip", "install", "--target", temp, `uv==${version}`], { env, timeoutMs: TOOL_INSTALL_TIMEOUT_MS })),
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* Prepends a directory to a PATH value.
|
|
373
|
+
*
|
|
374
|
+
* @param dir - Directory to put first.
|
|
375
|
+
* @param path - Existing PATH, if any.
|
|
376
|
+
* @returns The new PATH.
|
|
377
|
+
*/
|
|
378
|
+
function prependPath(dir, path) {
|
|
379
|
+
return path ? `${dir}:${path}` : dir;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Installs the pinned tools: node first, then the rest in series. A non-exact node version must
|
|
383
|
+
* be resolved first (`resolveVersions`).
|
|
384
|
+
*
|
|
385
|
+
* @param versions - Installable versions by tool.
|
|
386
|
+
* @param toolsDir - Root directory holding all tools.
|
|
387
|
+
* @param run - Process runner.
|
|
388
|
+
* @param env - Environment for child processes.
|
|
389
|
+
* @returns One result per tool that needed installing; tools already present are skipped.
|
|
390
|
+
* @example
|
|
391
|
+
* await installTools({ node: "24.14.1" }, "/opt/agent-cloud-kit", runProcess, process.env);
|
|
392
|
+
*/
|
|
393
|
+
export async function installTools(versions, toolsDir, run, env) {
|
|
394
|
+
const results = [];
|
|
395
|
+
let childEnv = env;
|
|
396
|
+
for (const tool of TOOLS) {
|
|
397
|
+
const version = versions[tool];
|
|
398
|
+
if (!version)
|
|
399
|
+
continue;
|
|
400
|
+
if (!isValidVersion(version)) {
|
|
401
|
+
results.push(await step(`${tool} ${version}`, async () => assertVersion(tool, version)));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (tool === "node" && !NODE_EXACT.test(version)) {
|
|
405
|
+
results.push(await step(`${tool} ${version}`, async () => {
|
|
406
|
+
throw new Error(`node ${version} must be resolved to an exact version first`);
|
|
407
|
+
}));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const installer = INSTALLERS[tool];
|
|
411
|
+
const binDir = installer.binDir(toolsDir, version);
|
|
412
|
+
if (!existsSync(join(binDir, EXECUTABLES[tool]))) {
|
|
413
|
+
results.push(await step(`${tool} ${version}`, () => installer.install(toolsDir, version, run, childEnv)));
|
|
414
|
+
}
|
|
415
|
+
if (tool === "node")
|
|
416
|
+
childEnv = { ...env, PATH: prependPath(binDir, env.PATH) };
|
|
417
|
+
}
|
|
418
|
+
return results;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Lists the bin directories of the pinned tools, in `TOOLS` order. A non-exact node version maps
|
|
422
|
+
* to the exact version setup resolved it to; invalid or unresolved versions are skipped.
|
|
423
|
+
*
|
|
424
|
+
* @param versions - Requested versions by tool.
|
|
425
|
+
* @param toolsDir - Root directory holding all tools.
|
|
426
|
+
* @param resolved - Versions setup installed (`MachineState.resolved`); empty when unknown.
|
|
427
|
+
* @returns The bin directories to put on PATH.
|
|
428
|
+
* @example
|
|
429
|
+
* toolBinDirs({ node: "22" }, "/opt/agent-cloud-kit", { node: "22.12.0" }); // ["/opt/agent-cloud-kit/node-22.12.0/bin"]
|
|
430
|
+
*/
|
|
431
|
+
export function toolBinDirs(versions, toolsDir, resolved = {}) {
|
|
432
|
+
return TOOLS.flatMap((tool) => {
|
|
433
|
+
const requested = versions[tool];
|
|
434
|
+
const version = requested ? installedVersion(tool, requested, resolved) : undefined;
|
|
435
|
+
return version ? [INSTALLERS[tool].binDir(toolsDir, version)] : [];
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Finds the bin directory that holds a pinned tool's executable.
|
|
440
|
+
*
|
|
441
|
+
* @param tool - Tool name.
|
|
442
|
+
* @param requested - Version as written in the repo.
|
|
443
|
+
* @param toolsDir - Root directory holding all tools.
|
|
444
|
+
* @param resolved - Versions setup installed (`MachineState.resolved`).
|
|
445
|
+
* @returns The executable path, or undefined when the version is invalid or not resolved.
|
|
446
|
+
* @example
|
|
447
|
+
* toolExecutable("node", "24.14.1", "/opt/agent-cloud-kit", {}); // "/opt/agent-cloud-kit/node-24.14.1/bin/node"
|
|
448
|
+
*/
|
|
449
|
+
export function toolExecutable(tool, requested, toolsDir, resolved) {
|
|
450
|
+
const version = installedVersion(tool, requested, resolved);
|
|
451
|
+
return version ? join(INSTALLERS[tool].binDir(toolsDir, version), EXECUTABLES[tool]) : undefined;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Builds the shell line that puts directories first on PATH.
|
|
455
|
+
*
|
|
456
|
+
* @param dirs - Directories, in order.
|
|
457
|
+
* @returns An `export PATH=...` line with a literal `$PATH`, or an empty string when there are none.
|
|
458
|
+
* @example
|
|
459
|
+
* pathLine(["/opt/agent-cloud-kit/node-24.14.1/bin"]); // export PATH="/opt/agent-cloud-kit/node-24.14.1/bin:$PATH"
|
|
460
|
+
*/
|
|
461
|
+
export function pathLine(dirs) {
|
|
462
|
+
return dirs.length ? `export PATH="${dirs.join(":")}:$PATH"` : "";
|
|
463
|
+
}
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Finds the cloned repos, decides per repo whether this version runs setup or relaunches the
|
|
3
|
+
* version the repo pins, and builds the paths and commands for that relaunch and for delegation.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { CONFIG_FILE, PACKAGE } from "./constants.js";
|
|
8
|
+
/** Directory, inside the tools dir, that holds one installed copy of this package per version. */
|
|
9
|
+
const PKG_DIR = "pkg";
|
|
10
|
+
/** Name of the CLI executable. */
|
|
11
|
+
const BIN_NAME = "agent-cloud-kit";
|
|
12
|
+
/**
|
|
13
|
+
* Tells whether a path is a directory, following symlinks; never throws.
|
|
14
|
+
*
|
|
15
|
+
* @param path - Path to check.
|
|
16
|
+
* @returns True for a directory.
|
|
17
|
+
*/
|
|
18
|
+
function isDirectory(path) {
|
|
19
|
+
try {
|
|
20
|
+
return statSync(path).isDirectory();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lists the first-level folders of a home directory that are git repos.
|
|
28
|
+
*
|
|
29
|
+
* @param home - Directory holding the clones.
|
|
30
|
+
* @returns Sorted absolute paths; empty when the directory cannot be read.
|
|
31
|
+
* @example
|
|
32
|
+
* findRepos("/home/user"); // ["/home/user/app"]
|
|
33
|
+
*/
|
|
34
|
+
export function findRepos(home) {
|
|
35
|
+
let names;
|
|
36
|
+
try {
|
|
37
|
+
names = readdirSync(home);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return names
|
|
43
|
+
.map((name) => join(home, name))
|
|
44
|
+
.filter((path) => isDirectory(path) && existsSync(join(path, ".git")))
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decides, for each repo, whether to run setup here, relaunch another version or skip it.
|
|
49
|
+
*
|
|
50
|
+
* @param repos - Repo roots.
|
|
51
|
+
* @param current - Version of this package.
|
|
52
|
+
* @param read - Config reader (`readConfig`).
|
|
53
|
+
* @returns One item per repo, in the same order.
|
|
54
|
+
* @example
|
|
55
|
+
* planSetup(["/home/user/app"], "1.0.0", readConfig); // [{ repo: "/home/user/app", action: "run", version: "1.0.0" }]
|
|
56
|
+
*/
|
|
57
|
+
export function planSetup(repos, current, read) {
|
|
58
|
+
return repos.map((repo) => {
|
|
59
|
+
const { config, errors } = read(repo);
|
|
60
|
+
if (!config)
|
|
61
|
+
return { repo, action: "skip", reason: errors.length ? errors.join("; ") : `no ${CONFIG_FILE}` };
|
|
62
|
+
if (config.version === current)
|
|
63
|
+
return { repo, action: "run", version: current };
|
|
64
|
+
return { repo, action: "relaunch", version: config.version };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Runs setup for one repo with the exact version it pins, through npx.
|
|
69
|
+
* The version is already validated as exact semver and travels as one argv element.
|
|
70
|
+
*
|
|
71
|
+
* @param repo - Repo root.
|
|
72
|
+
* @param version - Exact version from the repo config.
|
|
73
|
+
* @param run - Process runner.
|
|
74
|
+
* @param env - Environment for npx.
|
|
75
|
+
* @returns Resolves when the relaunched setup exits with 0.
|
|
76
|
+
* @throws When npx cannot run or exits with a non-zero code.
|
|
77
|
+
* @example
|
|
78
|
+
* await relaunch("/home/user/app", "1.2.0", runProcess, process.env);
|
|
79
|
+
*/
|
|
80
|
+
export function relaunch(repo, version, run, env) {
|
|
81
|
+
return run("npx", ["-y", `${PACKAGE}@${version}`, "setup", "--repo", repo], { env });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Locates the directory where setup installs a given version of this package.
|
|
85
|
+
*
|
|
86
|
+
* @param toolsDir - Tools directory.
|
|
87
|
+
* @param version - Package version.
|
|
88
|
+
* @returns `<toolsDir>/pkg/<version>`.
|
|
89
|
+
* @example
|
|
90
|
+
* packageDir("/opt/agent-cloud-kit", "1.0.0"); // "/opt/agent-cloud-kit/pkg/1.0.0"
|
|
91
|
+
*/
|
|
92
|
+
export function packageDir(toolsDir, version) {
|
|
93
|
+
return join(toolsDir, PKG_DIR, version);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Locates the CLI executable of an installed version of this package.
|
|
97
|
+
*
|
|
98
|
+
* @param toolsDir - Tools directory.
|
|
99
|
+
* @param version - Package version.
|
|
100
|
+
* @returns `<toolsDir>/pkg/<version>/node_modules/.bin/agent-cloud-kit`.
|
|
101
|
+
* @example
|
|
102
|
+
* packageBin("/opt/agent-cloud-kit", "1.0.0"); // "/opt/agent-cloud-kit/pkg/1.0.0/node_modules/.bin/agent-cloud-kit"
|
|
103
|
+
*/
|
|
104
|
+
export function packageBin(toolsDir, version) {
|
|
105
|
+
return join(packageDir(toolsDir, version), "node_modules", ".bin", BIN_NAME);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Locates the copy of this package installed for a version.
|
|
109
|
+
*
|
|
110
|
+
* @param toolsDir - Tools directory.
|
|
111
|
+
* @param version - Package version.
|
|
112
|
+
* @returns `<toolsDir>/pkg/<version>/node_modules/agent-cloud-kit`.
|
|
113
|
+
* @example
|
|
114
|
+
* packageInstallDir("/opt/agent-cloud-kit", "1.0.0"); // "/opt/agent-cloud-kit/pkg/1.0.0/node_modules/agent-cloud-kit"
|
|
115
|
+
*/
|
|
116
|
+
export function packageInstallDir(toolsDir, version) {
|
|
117
|
+
return join(packageDir(toolsDir, version), "node_modules", ...PACKAGE.split("/"));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Locates the directory of the stable link, which `session` puts on PATH.
|
|
121
|
+
*
|
|
122
|
+
* @param toolsDir - Tools directory.
|
|
123
|
+
* @returns `<toolsDir>/bin`.
|
|
124
|
+
* @example
|
|
125
|
+
* linkDir("/opt/agent-cloud-kit"); // "/opt/agent-cloud-kit/bin"
|
|
126
|
+
*/
|
|
127
|
+
export function linkDir(toolsDir) {
|
|
128
|
+
return join(toolsDir, "bin");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Locates the stable link that the repo hook calls.
|
|
132
|
+
*
|
|
133
|
+
* @param toolsDir - Tools directory.
|
|
134
|
+
* @returns `<toolsDir>/bin/agent-cloud-kit`.
|
|
135
|
+
* @example
|
|
136
|
+
* linkPath("/opt/agent-cloud-kit"); // "/opt/agent-cloud-kit/bin/agent-cloud-kit"
|
|
137
|
+
*/
|
|
138
|
+
export function linkPath(toolsDir) {
|
|
139
|
+
return join(linkDir(toolsDir), BIN_NAME);
|
|
140
|
+
}
|