@kici-dev/agent 0.1.10 → 0.1.11
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/execution/dep-installer.d.ts +30 -22
- package/dist/execution/dep-packer.d.ts +20 -13
- package/dist/execution/dep-restore.d.ts +9 -5
- package/dist/execution/validate-kici-deps.d.ts +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +522 -2
- package/dist/server.js +511 -138
- package/dist/workflow-runner.js +363 -103
- package/package.json +4 -4
- package/sbom.spdx.json +48 -48
package/dist/workflow-runner.js
CHANGED
|
@@ -5,19 +5,21 @@ import { register } from "node:module";
|
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
|
-
import fsPromises, { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
8
|
+
import fsPromises, { access, mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
9
9
|
import os, { tmpdir } from "node:os";
|
|
10
|
-
import path, { dirname, join } from "node:path";
|
|
10
|
+
import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
11
11
|
import { $ } from "zx";
|
|
12
12
|
import { createLogger, deriveSharedSecret, initZx, normalizeLineEndings, sha256, sha256File, toErrorMessage } from "@kici-dev/shared";
|
|
13
13
|
import { ExecutionJobStatus, ExecutionStepStatus } from "@kici-dev/engine";
|
|
14
14
|
import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
15
|
-
import "node:child_process";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
16
|
import { Readable, Transform } from "node:stream";
|
|
17
17
|
import { pipeline } from "node:stream/promises";
|
|
18
18
|
import { createGunzip } from "node:zlib";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
20
|
import { x } from "tar";
|
|
21
|
+
import { promisify } from "node:util";
|
|
22
|
+
import { PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
|
|
21
23
|
import https from "node:https";
|
|
22
24
|
import http from "node:http";
|
|
23
25
|
import.meta.url;
|
|
@@ -868,8 +870,8 @@ const SCRATCH_DIR_GIT_EXCLUDE_GLOB = `.kici/${SCRATCH_DIR_BASENAME_PREFIX}*`;
|
|
|
868
870
|
* We sidestep the race entirely by extracting each attempt into a unique
|
|
869
871
|
* scratch dir under `.kici/`. Failed attempts leave orphan scratch dirs whose
|
|
870
872
|
* draining writes are harmless — the next attempt does not touch them. On
|
|
871
|
-
* success
|
|
872
|
-
* (atomic on the same filesystem), then best-effort
|
|
873
|
+
* success `moveScratchIntoRepo` renames the extracted entries into place
|
|
874
|
+
* (atomic on the same filesystem), then best-effort cleans the scratch dir.
|
|
873
875
|
*
|
|
874
876
|
* Scratch dirs land inside the customer's cloned working tree, so the clone
|
|
875
877
|
* phase registers `SCRATCH_DIR_GIT_EXCLUDE_GLOB` in `.git/info/exclude` to
|
|
@@ -951,13 +953,58 @@ function resolveOrchestratorUrl(url) {
|
|
|
951
953
|
}
|
|
952
954
|
}
|
|
953
955
|
/**
|
|
954
|
-
*
|
|
956
|
+
* Move a fully-extracted scratch tree into the cloned repo. The dep tarball is
|
|
957
|
+
* packed repo-root-relative, so the scratch holds repo-root entries:
|
|
958
|
+
* `.kici/node_modules` for every manager, plus (for pnpm) the root
|
|
959
|
+
* `node_modules/.pnpm` store and in-repo workspace sibling dirs. `.kici/` itself
|
|
960
|
+
* already exists in the work tree (cloned or source-restored), so its children
|
|
961
|
+
* are moved individually; every other top-level entry is moved wholesale.
|
|
962
|
+
*
|
|
963
|
+
* On a cache-hit execution agent the destinations do not pre-exist (source
|
|
964
|
+
* restore excludes node_modules and never carries sibling dirs), so the renames
|
|
965
|
+
* have nothing to race; the defensive `rm` covers re-runs.
|
|
966
|
+
*/
|
|
967
|
+
async function moveScratchIntoRepo(scratchDir, workDir) {
|
|
968
|
+
for (const child of await fsPromises.readdir(scratchDir)) if (child === ".kici") {
|
|
969
|
+
const kiciScratch = join(scratchDir, ".kici");
|
|
970
|
+
for (const sub of await fsPromises.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
|
|
971
|
+
} else await moveInto(join(scratchDir, child), join(workDir, child));
|
|
972
|
+
}
|
|
973
|
+
/** Move `src` to `dest`, creating the parent and clearing any stale dest. */
|
|
974
|
+
async function moveInto(src, dest) {
|
|
975
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
976
|
+
await fsPromises.rm(dest, {
|
|
977
|
+
recursive: true,
|
|
978
|
+
force: true
|
|
979
|
+
});
|
|
980
|
+
await fsPromises.rename(src, dest);
|
|
981
|
+
}
|
|
982
|
+
/** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
|
|
983
|
+
async function cleanupScratch(scratchDir) {
|
|
984
|
+
try {
|
|
985
|
+
await fsPromises.rm(scratchDir, {
|
|
986
|
+
recursive: true,
|
|
987
|
+
force: true
|
|
988
|
+
});
|
|
989
|
+
} catch (cleanupErr) {
|
|
990
|
+
logger$3.warn("Scratch dir cleanup failed (orphan left behind)", {
|
|
991
|
+
scratchDir,
|
|
992
|
+
error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Restore dependencies from a cached tarball into the cloned repo.
|
|
955
998
|
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
999
|
+
* The tarball is packed repo-root-relative (see `dep-packer.ts`): every manager
|
|
1000
|
+
* carries `.kici/node_modules`; pnpm additionally carries the root
|
|
1001
|
+
* `node_modules/.pnpm` store and the in-repo workspace siblings `.kici` resolves.
|
|
1002
|
+
* Restore extracts into a scratch dir, then moves each entry into place — one
|
|
1003
|
+
* code path for all managers.
|
|
959
1004
|
*
|
|
960
|
-
* For
|
|
1005
|
+
* For HTTP/HTTPS URLs: a streaming pipeline (response -> hash -> gunzip -> tar)
|
|
1006
|
+
* with a 5-minute timeout and up to 2 retries avoids buffering whole tarballs.
|
|
1007
|
+
* For file:// URLs: a buffer-based approach (local, no streaming benefit).
|
|
961
1008
|
*
|
|
962
1009
|
* @param workDir - Root directory of the cloned repository
|
|
963
1010
|
* @param depsUrl - URL to the dependency tarball (http://, https://, or file://)
|
|
@@ -974,11 +1021,14 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
974
1021
|
const actualHash = computeHash(data);
|
|
975
1022
|
if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
|
|
976
1023
|
}
|
|
977
|
-
|
|
1024
|
+
const scratchDir = join(kiciDir, `${SCRATCH_DIR_BASENAME_PREFIX}${process.pid}-file-${Date.now()}`);
|
|
1025
|
+
await extractTarball(data, scratchDir);
|
|
1026
|
+
await moveScratchIntoRepo(scratchDir, workDir);
|
|
1027
|
+
await cleanupScratch(scratchDir);
|
|
978
1028
|
const sizeMB = (data.length / (1024 * 1024)).toFixed(2);
|
|
979
1029
|
logger$3.info("Dependencies restored from cache (file)", {
|
|
980
1030
|
sizeMB,
|
|
981
|
-
targetDir:
|
|
1031
|
+
targetDir: workDir
|
|
982
1032
|
});
|
|
983
1033
|
return;
|
|
984
1034
|
}
|
|
@@ -992,19 +1042,9 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
992
1042
|
try {
|
|
993
1043
|
const { scratchDir, hash } = await extractIntoScratch(depsUrl, kiciDir, attempt);
|
|
994
1044
|
if (depsHash && hash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${hash}`);
|
|
995
|
-
await
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
recursive: true,
|
|
999
|
-
force: true
|
|
1000
|
-
});
|
|
1001
|
-
} catch (cleanupErr) {
|
|
1002
|
-
logger$3.warn("Scratch dir cleanup failed (orphan left behind)", {
|
|
1003
|
-
scratchDir,
|
|
1004
|
-
error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
logger$3.info("Dependencies restored from cache (stream)", { targetDir: kiciDir });
|
|
1045
|
+
await moveScratchIntoRepo(scratchDir, workDir);
|
|
1046
|
+
await cleanupScratch(scratchDir);
|
|
1047
|
+
logger$3.info("Dependencies restored from cache (stream)", { targetDir: workDir });
|
|
1008
1048
|
return;
|
|
1009
1049
|
} catch (err) {
|
|
1010
1050
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1163,48 +1203,190 @@ function redactNpmOutput(input, tokens) {
|
|
|
1163
1203
|
}
|
|
1164
1204
|
return out;
|
|
1165
1205
|
}
|
|
1206
|
+
const LOCAL_PROTOCOLS = [
|
|
1207
|
+
"workspace:",
|
|
1208
|
+
"file:",
|
|
1209
|
+
"link:",
|
|
1210
|
+
"portal:"
|
|
1211
|
+
];
|
|
1212
|
+
/** The dependency maps a package manager resolves in `.kici/package.json`. */
|
|
1213
|
+
const DEP_FIELDS = [
|
|
1214
|
+
"dependencies",
|
|
1215
|
+
"devDependencies",
|
|
1216
|
+
"optionalDependencies",
|
|
1217
|
+
"peerDependencies"
|
|
1218
|
+
];
|
|
1219
|
+
/**
|
|
1220
|
+
* Scan a parsed `.kici/package.json` for dependency specifiers that use a
|
|
1221
|
+
* local protocol (`workspace:`/`file:`/`link:`/`portal:`). Returns one entry
|
|
1222
|
+
* per dependency, in field order. Returns an empty array when there are none.
|
|
1223
|
+
*/
|
|
1224
|
+
function findLocalProtocolDeps(pkg) {
|
|
1225
|
+
const found = [];
|
|
1226
|
+
for (const field of DEP_FIELDS) {
|
|
1227
|
+
const deps = pkg[field];
|
|
1228
|
+
if (!deps || typeof deps !== "object") continue;
|
|
1229
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
1230
|
+
if (typeof spec !== "string") continue;
|
|
1231
|
+
const protocol = LOCAL_PROTOCOLS.find((proto) => spec.startsWith(proto));
|
|
1232
|
+
if (protocol) found.push({
|
|
1233
|
+
name,
|
|
1234
|
+
spec,
|
|
1235
|
+
protocol
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return found;
|
|
1240
|
+
}
|
|
1241
|
+
/** Parse `.kici/package.json`, returning `null` when it is missing or invalid. */
|
|
1242
|
+
async function readKiciPackageJson(kiciDir) {
|
|
1243
|
+
let raw;
|
|
1244
|
+
try {
|
|
1245
|
+
raw = await readFile(join(kiciDir, "package.json"), "utf-8");
|
|
1246
|
+
} catch {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
return JSON.parse(raw);
|
|
1251
|
+
} catch {
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Whether `.kici/package.json` declares any local-protocol dependency. Used to
|
|
1257
|
+
* decide whether the agent must build the in-repo workspace dependency closure
|
|
1258
|
+
* after a pnpm install (so a `workspace:` sibling's build output exists before
|
|
1259
|
+
* the workflow that imports it loads).
|
|
1260
|
+
*/
|
|
1261
|
+
async function kiciHasLocalProtocolDeps(kiciDir) {
|
|
1262
|
+
const pkg = await readKiciPackageJson(kiciDir);
|
|
1263
|
+
if (!pkg) return false;
|
|
1264
|
+
return findLocalProtocolDeps(pkg).length > 0;
|
|
1265
|
+
}
|
|
1266
|
+
async function fileExists$1(target) {
|
|
1267
|
+
try {
|
|
1268
|
+
await access(target);
|
|
1269
|
+
return true;
|
|
1270
|
+
} catch {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
|
|
1275
|
+
function resolveLocalPath(kiciDir, dep) {
|
|
1276
|
+
const rawPath = dep.spec.slice(dep.protocol.length);
|
|
1277
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(kiciDir, rawPath);
|
|
1278
|
+
}
|
|
1279
|
+
/** Whether `target` is `repoRoot` itself or a path inside it. */
|
|
1280
|
+
function isInsideRepo(repoRoot, target) {
|
|
1281
|
+
const rel = relative(repoRoot, target);
|
|
1282
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Classify each local-protocol dependency for the detected package manager and
|
|
1286
|
+
* return the ones that are unresolvable in the agent's single-clone model.
|
|
1287
|
+
*/
|
|
1288
|
+
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
1289
|
+
if (packageManager === PackageManager.Npm) return [...deps];
|
|
1290
|
+
const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
|
|
1291
|
+
const unresolvable = [];
|
|
1292
|
+
for (const dep of deps) {
|
|
1293
|
+
if (dep.protocol === "workspace:") {
|
|
1294
|
+
if (!hasWorkspaceFile) unresolvable.push(dep);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
|
|
1298
|
+
}
|
|
1299
|
+
return unresolvable;
|
|
1300
|
+
}
|
|
1301
|
+
/** Build the actionable error for unresolvable local-protocol dependencies. */
|
|
1302
|
+
function formatUnresolvableDepError(offenders, packageManager) {
|
|
1303
|
+
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
1304
|
+
if (packageManager === PackageManager.Npm) return `These .kici/ dependencies use local-protocol specifiers npm cannot resolve from a registry: ${list}. npm has no workspace protocol — pin a published version, publish the package to your registry, or use pnpm so an in-repo workspace sibling can be resolved.`;
|
|
1305
|
+
return `These .kici/ dependencies point outside the cloned repository, which the agent never has: ${list}. A workspace: dependency requires a pnpm-workspace.yaml at the repo root, and file:/link:/portal: paths must stay inside this repository.`;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Throw an actionable error when `.kici/package.json` declares a local-protocol
|
|
1309
|
+
* dependency the detected package manager cannot resolve from the single cloned
|
|
1310
|
+
* repository. A missing or unparseable package.json is left for the install to
|
|
1311
|
+
* report.
|
|
1312
|
+
*/
|
|
1313
|
+
async function assertResolvableDeps(args) {
|
|
1314
|
+
const pkg = await readKiciPackageJson(args.kiciDir);
|
|
1315
|
+
if (!pkg) return;
|
|
1316
|
+
const localDeps = findLocalProtocolDeps(pkg);
|
|
1317
|
+
if (localDeps.length === 0) return;
|
|
1318
|
+
const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
|
|
1319
|
+
if (offenders.length === 0) return;
|
|
1320
|
+
throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
|
|
1321
|
+
}
|
|
1166
1322
|
//#endregion
|
|
1167
1323
|
//#region src/execution/dep-installer.ts
|
|
1168
1324
|
/**
|
|
1169
1325
|
* Inline dependency installation for graceful degradation.
|
|
1170
1326
|
*
|
|
1171
|
-
* When dep cache is unavailable or download fails, the agent
|
|
1172
|
-
*
|
|
1327
|
+
* When the dep cache is unavailable or a download fails, the agent installs
|
|
1328
|
+
* `.kici/` dependencies directly with the repository's package manager.
|
|
1173
1329
|
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1330
|
+
* The package manager is detected from the cloned repo (npm / pnpm); the
|
|
1331
|
+
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
1332
|
+
* is the default and ships with every Node.js install; pnpm is used when the
|
|
1333
|
+
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
1334
|
+
* `workspace:` siblings. yarn is detected but not yet supported and is
|
|
1335
|
+
* rejected with an actionable error.
|
|
1176
1336
|
*
|
|
1177
|
-
* Security:
|
|
1178
|
-
* cache poisoning across build jobs
|
|
1179
|
-
* taint the cache used by subsequent builds.
|
|
1180
|
-
*
|
|
1181
|
-
* `--ignore-scripts` whenever a private
|
|
1337
|
+
* Security: the install runs with an isolated per-invocation cache/store
|
|
1338
|
+
* directory to prevent cache poisoning across build jobs — a malicious
|
|
1339
|
+
* package.json in one repo cannot taint the cache used by subsequent builds.
|
|
1340
|
+
* The same pressure rules out letting lifecycle scripts see synthesized auth
|
|
1341
|
+
* env vars — the install runs with `--ignore-scripts` whenever a private
|
|
1342
|
+
* registry is configured.
|
|
1182
1343
|
*/
|
|
1183
1344
|
const logger$2 = createLogger({ prefix: "dep-installer" });
|
|
1345
|
+
const execFileAsync = promisify(execFile);
|
|
1346
|
+
/** Install subprocess timeout (10 min) and stdout/stderr buffer (128 MiB). */
|
|
1347
|
+
const INSTALL_TIMEOUT_MS = 6e5;
|
|
1348
|
+
const INSTALL_MAX_BUFFER = 128 * 1024 * 1024;
|
|
1184
1349
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1350
|
+
* Detect the package manager for the cloned repo from its committed manifests.
|
|
1351
|
+
* A pnpm workspace's `packageManager` field + `pnpm-lock.yaml` live at the repo
|
|
1352
|
+
* root, so check there first; fall back to `.kici/` for a standalone
|
|
1353
|
+
* (non-workspace) project that carries its own lockfile; default to npm when
|
|
1354
|
+
* neither carries a signal. Uses the manifests-only detector so the agent's own
|
|
1355
|
+
* launch env (`npm_config_user_agent`) never leaks into the decision.
|
|
1356
|
+
*/
|
|
1357
|
+
async function detectKiciPackageManager(repoRoot, kiciDir) {
|
|
1358
|
+
return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Install `.kici/` dependencies inline with the repo's package manager.
|
|
1188
1362
|
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1363
|
+
* Falls back to this when the dep cache is unavailable or a download fails.
|
|
1364
|
+
* The install runs with an isolated cache/store directory (created in
|
|
1365
|
+
* `os.tmpdir()`) to prevent cache poisoning between build jobs; the directory
|
|
1366
|
+
* is removed after installation.
|
|
1191
1367
|
*
|
|
1192
|
-
* If `opts.npmRegistries` / `opts.installEnvSecrets` is provided,
|
|
1193
|
-
*
|
|
1194
|
-
* the
|
|
1195
|
-
*
|
|
1196
|
-
* synthesized token env vars.
|
|
1368
|
+
* If `opts.npmRegistries` / `opts.installEnvSecrets` is provided, a job-scoped
|
|
1369
|
+
* `.kici/.npmrc` overlay is synthesized for the install, restored in `finally`,
|
|
1370
|
+
* and the install runs with `--ignore-scripts` so lifecycle scripts in a
|
|
1371
|
+
* committed `package.json` cannot exfiltrate the synthesized token env vars.
|
|
1197
1372
|
*
|
|
1198
|
-
* @param kiciDir - Path to the
|
|
1199
|
-
* @param opts - Optional registry / installEnv configuration.
|
|
1200
|
-
* behavior is identical to the pre-private-registry version.
|
|
1373
|
+
* @param kiciDir - Path to the `.kici/` directory containing package.json.
|
|
1374
|
+
* @param opts - Optional registry / installEnv / repoRoot configuration.
|
|
1201
1375
|
*/
|
|
1202
1376
|
async function installDeps(kiciDir, opts = {}) {
|
|
1377
|
+
const repoRoot = opts.repoRoot ?? dirname(kiciDir);
|
|
1378
|
+
const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
|
|
1203
1379
|
logger$2.info("Installing deps inline", {
|
|
1204
|
-
packageManager
|
|
1380
|
+
packageManager,
|
|
1205
1381
|
dir: kiciDir
|
|
1206
1382
|
});
|
|
1207
|
-
process.stderr.write(`[dep-installer:trace] starting install: pm
|
|
1383
|
+
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
1384
|
+
if (packageManager === PackageManager.Yarn) throw new Error("This repository uses yarn, which the KiCI agent does not yet support for .kici/ dependency installation. Use npm or pnpm for the .kici/ project, or open a feature request for yarn support.");
|
|
1385
|
+
await assertResolvableDeps({
|
|
1386
|
+
kiciDir,
|
|
1387
|
+
repoRoot,
|
|
1388
|
+
packageManager
|
|
1389
|
+
});
|
|
1208
1390
|
const startTime = Date.now();
|
|
1209
1391
|
const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
|
|
1210
1392
|
const registryConfig = await applyNpmRegistryConfig({
|
|
@@ -1214,71 +1396,149 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
1214
1396
|
jobIdShort: opts.jobIdShort ?? "00000000"
|
|
1215
1397
|
});
|
|
1216
1398
|
try {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const { promisify } = await import("node:util");
|
|
1228
|
-
const execFileAsync = promisify(execFile);
|
|
1229
|
-
const buildArgs = (...prefix) => {
|
|
1230
|
-
const args = [
|
|
1231
|
-
...prefix,
|
|
1232
|
-
npmCmd,
|
|
1233
|
-
"--cache",
|
|
1234
|
-
cacheDir,
|
|
1235
|
-
"--no-audit",
|
|
1236
|
-
"--no-fund"
|
|
1237
|
-
];
|
|
1238
|
-
if (hasPrivateRegistry) args.push("--ignore-scripts");
|
|
1239
|
-
return args;
|
|
1240
|
-
};
|
|
1241
|
-
try {
|
|
1242
|
-
if (npmCliPath) {
|
|
1243
|
-
process.stderr.write(`[dep-installer:trace] running: ${nodeExe} ${npmCliPath} ${buildArgs().join(" ")}\n`);
|
|
1244
|
-
await execFileAsync(nodeExe, buildArgs(npmCliPath), {
|
|
1245
|
-
cwd: kiciDir,
|
|
1246
|
-
env: envWithNode,
|
|
1247
|
-
timeout: 6e5,
|
|
1248
|
-
maxBuffer: 128 * 1024 * 1024
|
|
1249
|
-
});
|
|
1250
|
-
} else {
|
|
1251
|
-
process.stderr.write(`[dep-installer:trace] running: npm ${buildArgs().join(" ")}\n`);
|
|
1252
|
-
await execFileAsync("npm", buildArgs(), {
|
|
1253
|
-
cwd: kiciDir,
|
|
1254
|
-
env: envWithNode,
|
|
1255
|
-
timeout: 6e5,
|
|
1256
|
-
maxBuffer: 128 * 1024 * 1024
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
|
-
} finally {
|
|
1260
|
-
await rm(cacheDir, {
|
|
1261
|
-
recursive: true,
|
|
1262
|
-
force: true
|
|
1263
|
-
}).catch(() => {});
|
|
1264
|
-
}
|
|
1399
|
+
if (packageManager === PackageManager.Pnpm) await runPnpmInstall({
|
|
1400
|
+
kiciDir,
|
|
1401
|
+
hasPrivateRegistry,
|
|
1402
|
+
registryConfig
|
|
1403
|
+
});
|
|
1404
|
+
else await runNpmInstall({
|
|
1405
|
+
kiciDir,
|
|
1406
|
+
hasPrivateRegistry,
|
|
1407
|
+
registryConfig
|
|
1408
|
+
});
|
|
1265
1409
|
} catch (e) {
|
|
1266
|
-
const msg = toErrorMessage(e);
|
|
1267
1410
|
const tokens = registryConfig.tokensForRedaction;
|
|
1268
|
-
process.stderr.write(`[dep-installer:trace] INSTALL FAILED: ${redactNpmOutput(
|
|
1269
|
-
|
|
1270
|
-
if (e && typeof e === "object" && "stderr" in e) process.stderr.write(`[dep-installer:trace] stderr: ${redactNpmOutput(String(e.stderr), tokens).slice(0, 500)}\n`);
|
|
1411
|
+
process.stderr.write(`[dep-installer:trace] INSTALL FAILED: ${redactNpmOutput(toErrorMessage(e), tokens)}\n`);
|
|
1412
|
+
logSubprocessStreams(e, tokens);
|
|
1271
1413
|
throw e;
|
|
1272
1414
|
} finally {
|
|
1273
1415
|
await registryConfig.cleanup();
|
|
1274
1416
|
}
|
|
1417
|
+
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
1275
1418
|
const durationMs = Date.now() - startTime;
|
|
1276
1419
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
1277
1420
|
logger$2.info("Deps installed inline", {
|
|
1278
|
-
packageManager
|
|
1421
|
+
packageManager,
|
|
1279
1422
|
durationMs
|
|
1280
1423
|
});
|
|
1281
1424
|
}
|
|
1425
|
+
/** Build the Node binary directory onto PATH so spawned tools find `node`. */
|
|
1426
|
+
function envWithNodeOnPath(extraEnv, nodeDir) {
|
|
1427
|
+
const { NODE_ENV: _NODE_ENV, ...restEnv } = process.env;
|
|
1428
|
+
return {
|
|
1429
|
+
...restEnv,
|
|
1430
|
+
...extraEnv,
|
|
1431
|
+
PATH: `${nodeDir}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
/** Run `npm install` in `.kici/` with an isolated cache directory. */
|
|
1435
|
+
async function runNpmInstall(args) {
|
|
1436
|
+
const { npmCliPath, nodeExe, nodeDir } = resolveNpm();
|
|
1437
|
+
const cacheDir = await mkdtemp(join(tmpdir(), "kici-npm-cache-"));
|
|
1438
|
+
const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
|
|
1439
|
+
const buildArgs = (...prefix) => {
|
|
1440
|
+
const a = [
|
|
1441
|
+
...prefix,
|
|
1442
|
+
"install",
|
|
1443
|
+
"--cache",
|
|
1444
|
+
cacheDir,
|
|
1445
|
+
"--no-audit",
|
|
1446
|
+
"--no-fund"
|
|
1447
|
+
];
|
|
1448
|
+
if (args.hasPrivateRegistry) a.push("--ignore-scripts");
|
|
1449
|
+
return a;
|
|
1450
|
+
};
|
|
1451
|
+
try {
|
|
1452
|
+
const bin = npmCliPath ? nodeExe : "npm";
|
|
1453
|
+
const argv = npmCliPath ? buildArgs(npmCliPath) : buildArgs();
|
|
1454
|
+
process.stderr.write(`[dep-installer:trace] running: ${bin} ${argv.join(" ")}\n`);
|
|
1455
|
+
await execFileAsync(bin, argv, {
|
|
1456
|
+
cwd: args.kiciDir,
|
|
1457
|
+
env,
|
|
1458
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
1459
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
1460
|
+
});
|
|
1461
|
+
} finally {
|
|
1462
|
+
await rm(cacheDir, {
|
|
1463
|
+
recursive: true,
|
|
1464
|
+
force: true
|
|
1465
|
+
}).catch(() => {});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Run `pnpm install` from `.kici/`. pnpm walks up to the workspace root, so a
|
|
1470
|
+
* `workspace:` sibling in the same cloned repo resolves. Uses an isolated store
|
|
1471
|
+
* (`--config.store-dir`) for cross-job isolation, `package-import-method=copy`
|
|
1472
|
+
* so the on-disk store is a self-contained tree of real files (a later dep
|
|
1473
|
+
* cache tars it), and disables interactive purge prompts + the side-effects
|
|
1474
|
+
* cache for deterministic, non-interactive runs.
|
|
1475
|
+
*/
|
|
1476
|
+
async function runPnpmInstall(args) {
|
|
1477
|
+
await assertPnpmAvailable();
|
|
1478
|
+
const { nodeDir } = resolveNpm();
|
|
1479
|
+
const storeDir = await mkdtemp(join(tmpdir(), "kici-pnpm-store-"));
|
|
1480
|
+
const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
|
|
1481
|
+
const argv = [
|
|
1482
|
+
"install",
|
|
1483
|
+
`--config.store-dir=${storeDir}`,
|
|
1484
|
+
"--config.package-import-method=copy",
|
|
1485
|
+
"--config.confirm-modules-purge=false",
|
|
1486
|
+
"--config.side-effects-cache=false"
|
|
1487
|
+
];
|
|
1488
|
+
if (args.hasPrivateRegistry) argv.push("--ignore-scripts");
|
|
1489
|
+
try {
|
|
1490
|
+
process.stderr.write(`[dep-installer:trace] running: pnpm ${argv.join(" ")}\n`);
|
|
1491
|
+
await execFileAsync("pnpm", argv, {
|
|
1492
|
+
cwd: args.kiciDir,
|
|
1493
|
+
env,
|
|
1494
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
1495
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
1496
|
+
});
|
|
1497
|
+
} finally {
|
|
1498
|
+
await rm(storeDir, {
|
|
1499
|
+
recursive: true,
|
|
1500
|
+
force: true
|
|
1501
|
+
}).catch(() => {});
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Build the in-repo dependency closure of the `.kici/` package so a
|
|
1506
|
+
* `workspace:` sibling's build output exists before the workflow that imports
|
|
1507
|
+
* it loads. `--filter "{.kici}^..."` selects only `.kici`'s dependencies (not
|
|
1508
|
+
* `.kici` itself); `--if-present` skips siblings without a build script. Runs
|
|
1509
|
+
* with a clean env (no synthesized registry tokens).
|
|
1510
|
+
*/
|
|
1511
|
+
async function buildWorkspaceClosure(repoRoot) {
|
|
1512
|
+
const { nodeDir } = resolveNpm();
|
|
1513
|
+
const env = envWithNodeOnPath({}, nodeDir);
|
|
1514
|
+
const argv = [
|
|
1515
|
+
"--filter",
|
|
1516
|
+
"{.kici}^...",
|
|
1517
|
+
"run",
|
|
1518
|
+
"build",
|
|
1519
|
+
"--if-present"
|
|
1520
|
+
];
|
|
1521
|
+
process.stderr.write(`[dep-installer:trace] building workspace closure: pnpm ${argv.join(" ")}\n`);
|
|
1522
|
+
await execFileAsync("pnpm", argv, {
|
|
1523
|
+
cwd: repoRoot,
|
|
1524
|
+
env,
|
|
1525
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
1526
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
/** Throw an actionable error when the repo needs pnpm but it is not installed. */
|
|
1530
|
+
async function assertPnpmAvailable() {
|
|
1531
|
+
try {
|
|
1532
|
+
await execFileAsync("pnpm", ["--version"], { timeout: 3e4 });
|
|
1533
|
+
} catch (e) {
|
|
1534
|
+
throw new Error(`This repository is a pnpm workspace, but pnpm is not available on this agent. Install pnpm (e.g. \`corepack enable\`) or run on a container/ Firecracker agent that bundles it. (${toErrorMessage(e)})`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/** Trace the redacted stdout/stderr of a failed install subprocess. */
|
|
1538
|
+
function logSubprocessStreams(e, tokens) {
|
|
1539
|
+
if (e && typeof e === "object" && "stdout" in e) process.stderr.write(`[dep-installer:trace] stdout: ${redactNpmOutput(String(e.stdout), tokens).slice(0, 500)}\n`);
|
|
1540
|
+
if (e && typeof e === "object" && "stderr" in e) process.stderr.write(`[dep-installer:trace] stderr: ${redactNpmOutput(String(e.stderr), tokens).slice(0, 500)}\n`);
|
|
1541
|
+
}
|
|
1282
1542
|
//#endregion
|
|
1283
1543
|
//#region src/execution/workflow-loader.ts
|
|
1284
1544
|
/**
|
|
@@ -1288,7 +1548,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
1288
1548
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
1289
1549
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
1290
1550
|
*/
|
|
1291
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
1551
|
+
const AGENT_SDK_VERSION = "0.1.11";
|
|
1292
1552
|
const AGENT_SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
|
|
1293
1553
|
/**
|
|
1294
1554
|
* Register the shared oxc-transform ESM loader hook so subsequent dynamic
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kici-dev/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Customer-deployable agent for the KiCI CI/CD stack. Connects to an orchestrator, clones the workflow repo, executes steps, and streams logs back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kici",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"ws": "^8.20.0",
|
|
58
58
|
"zod": "^4.3.6",
|
|
59
59
|
"zx": "^8.8.5",
|
|
60
|
-
"@kici-dev/
|
|
61
|
-
"@kici-dev/
|
|
62
|
-
"@kici-dev/shared": "0.1.
|
|
60
|
+
"@kici-dev/sdk": "0.1.11",
|
|
61
|
+
"@kici-dev/engine": "0.1.11",
|
|
62
|
+
"@kici-dev/shared": "0.1.11"
|
|
63
63
|
},
|
|
64
64
|
"kici": {
|
|
65
65
|
"metrics": {
|