@kici-dev/agent 0.1.10 → 0.1.12
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 +537 -2
- package/dist/server.js +526 -138
- package/dist/workflow-runner.js +378 -103
- package/package.json +4 -4
- package/sbom.spdx.json +48 -48
package/dist/server.js
CHANGED
|
@@ -12,14 +12,14 @@ import { RingBuffer, createHealthRoutes, createLogger, createMeter, createMetric
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
|
|
14
14
|
import { ALLOWED_SYSTEM_VARS, ExecutionJobStatus, ExecutionStepStatus, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, deriveOsArchLabels, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
|
|
15
|
-
import { execFileSync, execSync, fork, spawn } from "node:child_process";
|
|
15
|
+
import { execFile, execFileSync, execSync, fork, spawn } from "node:child_process";
|
|
16
16
|
import WebSocket from "ws";
|
|
17
17
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
-
import { format } from "node:util";
|
|
18
|
+
import { format, promisify } from "node:util";
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
|
-
import path, { dirname, join } from "node:path";
|
|
20
|
+
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
-
import fs, { access, mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
22
|
+
import fs, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
|
|
23
23
|
import Docker from "dockerode";
|
|
24
24
|
import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
|
|
25
25
|
import { c, x } from "tar";
|
|
@@ -27,6 +27,7 @@ import https from "node:https";
|
|
|
27
27
|
import http from "node:http";
|
|
28
28
|
import { pipeline } from "node:stream/promises";
|
|
29
29
|
import { createGunzip } from "node:zlib";
|
|
30
|
+
import { PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
|
|
30
31
|
import { createInterface } from "node:readline";
|
|
31
32
|
var __defProp = Object.defineProperty;
|
|
32
33
|
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -974,14 +975,14 @@ var init_console_capture = __esmMin((() => {
|
|
|
974
975
|
init_console_capture();
|
|
975
976
|
function safe(name, fallback = "unknown") {
|
|
976
977
|
switch (name) {
|
|
977
|
-
case "version": return "0.1.
|
|
978
|
-
case "buildCommit": return "
|
|
979
|
-
case "sdkVersion": return "0.1.
|
|
978
|
+
case "version": return "0.1.12";
|
|
979
|
+
case "buildCommit": return "d220ac45c";
|
|
980
|
+
case "sdkVersion": return "0.1.12";
|
|
980
981
|
case "sdkBundleHash": return "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
|
|
981
|
-
case "sharedVersion": return "0.1.
|
|
982
|
+
case "sharedVersion": return "0.1.12";
|
|
982
983
|
case "sharedBundleHash": return "36a23a454fd75ec35f258d39f254543ec0451c6b543fe3618ce427e6e6369aae";
|
|
983
|
-
case "engineVersion": return "0.1.
|
|
984
|
-
case "engineBundleHash": return "
|
|
984
|
+
case "engineVersion": return "0.1.12";
|
|
985
|
+
case "engineBundleHash": return "4ab08e63bcb8558b1191c5d3ab8d58b71950b77d911af56c9955ae1d787c4302";
|
|
985
986
|
default: return fallback;
|
|
986
987
|
}
|
|
987
988
|
}
|
|
@@ -1628,7 +1629,7 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
1628
1629
|
}
|
|
1629
1630
|
var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
|
|
1630
1631
|
var init_workflow_loader = __esmMin((() => {
|
|
1631
|
-
AGENT_SDK_VERSION = "0.1.
|
|
1632
|
+
AGENT_SDK_VERSION = "0.1.12";
|
|
1632
1633
|
AGENT_SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
|
|
1633
1634
|
hookRegistered = false;
|
|
1634
1635
|
}));
|
|
@@ -1746,8 +1747,8 @@ async function streamFetchAndExtract(url, targetDir) {
|
|
|
1746
1747
|
* We sidestep the race entirely by extracting each attempt into a unique
|
|
1747
1748
|
* scratch dir under `.kici/`. Failed attempts leave orphan scratch dirs whose
|
|
1748
1749
|
* draining writes are harmless — the next attempt does not touch them. On
|
|
1749
|
-
* success
|
|
1750
|
-
* (atomic on the same filesystem), then best-effort
|
|
1750
|
+
* success `moveScratchIntoRepo` renames the extracted entries into place
|
|
1751
|
+
* (atomic on the same filesystem), then best-effort cleans the scratch dir.
|
|
1751
1752
|
*
|
|
1752
1753
|
* Scratch dirs land inside the customer's cloned working tree, so the clone
|
|
1753
1754
|
* phase registers `SCRATCH_DIR_GIT_EXCLUDE_GLOB` in `.git/info/exclude` to
|
|
@@ -1783,13 +1784,58 @@ function resolveOrchestratorUrl(url) {
|
|
|
1783
1784
|
}
|
|
1784
1785
|
}
|
|
1785
1786
|
/**
|
|
1786
|
-
*
|
|
1787
|
+
* Move a fully-extracted scratch tree into the cloned repo. The dep tarball is
|
|
1788
|
+
* packed repo-root-relative, so the scratch holds repo-root entries:
|
|
1789
|
+
* `.kici/node_modules` for every manager, plus (for pnpm) the root
|
|
1790
|
+
* `node_modules/.pnpm` store and in-repo workspace sibling dirs. `.kici/` itself
|
|
1791
|
+
* already exists in the work tree (cloned or source-restored), so its children
|
|
1792
|
+
* are moved individually; every other top-level entry is moved wholesale.
|
|
1787
1793
|
*
|
|
1788
|
-
*
|
|
1789
|
-
*
|
|
1790
|
-
*
|
|
1794
|
+
* On a cache-hit execution agent the destinations do not pre-exist (source
|
|
1795
|
+
* restore excludes node_modules and never carries sibling dirs), so the renames
|
|
1796
|
+
* have nothing to race; the defensive `rm` covers re-runs.
|
|
1797
|
+
*/
|
|
1798
|
+
async function moveScratchIntoRepo(scratchDir, workDir) {
|
|
1799
|
+
for (const child of await fs.readdir(scratchDir)) if (child === ".kici") {
|
|
1800
|
+
const kiciScratch = join(scratchDir, ".kici");
|
|
1801
|
+
for (const sub of await fs.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
|
|
1802
|
+
} else await moveInto(join(scratchDir, child), join(workDir, child));
|
|
1803
|
+
}
|
|
1804
|
+
/** Move `src` to `dest`, creating the parent and clearing any stale dest. */
|
|
1805
|
+
async function moveInto(src, dest) {
|
|
1806
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
1807
|
+
await fs.rm(dest, {
|
|
1808
|
+
recursive: true,
|
|
1809
|
+
force: true
|
|
1810
|
+
});
|
|
1811
|
+
await fs.rename(src, dest);
|
|
1812
|
+
}
|
|
1813
|
+
/** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
|
|
1814
|
+
async function cleanupScratch(scratchDir) {
|
|
1815
|
+
try {
|
|
1816
|
+
await fs.rm(scratchDir, {
|
|
1817
|
+
recursive: true,
|
|
1818
|
+
force: true
|
|
1819
|
+
});
|
|
1820
|
+
} catch (cleanupErr) {
|
|
1821
|
+
logger$8.warn("Scratch dir cleanup failed (orphan left behind)", {
|
|
1822
|
+
scratchDir,
|
|
1823
|
+
error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Restore dependencies from a cached tarball into the cloned repo.
|
|
1829
|
+
*
|
|
1830
|
+
* The tarball is packed repo-root-relative (see `dep-packer.ts`): every manager
|
|
1831
|
+
* carries `.kici/node_modules`; pnpm additionally carries the root
|
|
1832
|
+
* `node_modules/.pnpm` store and the in-repo workspace siblings `.kici` resolves.
|
|
1833
|
+
* Restore extracts into a scratch dir, then moves each entry into place — one
|
|
1834
|
+
* code path for all managers.
|
|
1791
1835
|
*
|
|
1792
|
-
* For
|
|
1836
|
+
* For HTTP/HTTPS URLs: a streaming pipeline (response -> hash -> gunzip -> tar)
|
|
1837
|
+
* with a 5-minute timeout and up to 2 retries avoids buffering whole tarballs.
|
|
1838
|
+
* For file:// URLs: a buffer-based approach (local, no streaming benefit).
|
|
1793
1839
|
*
|
|
1794
1840
|
* @param workDir - Root directory of the cloned repository
|
|
1795
1841
|
* @param depsUrl - URL to the dependency tarball (http://, https://, or file://)
|
|
@@ -1806,11 +1852,14 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
1806
1852
|
const actualHash = computeHash(data);
|
|
1807
1853
|
if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
|
|
1808
1854
|
}
|
|
1809
|
-
|
|
1855
|
+
const scratchDir = join(kiciDir, `${SCRATCH_DIR_BASENAME_PREFIX}${process.pid}-file-${Date.now()}`);
|
|
1856
|
+
await extractTarball(data, scratchDir);
|
|
1857
|
+
await moveScratchIntoRepo(scratchDir, workDir);
|
|
1858
|
+
await cleanupScratch(scratchDir);
|
|
1810
1859
|
const sizeMB = (data.length / (1024 * 1024)).toFixed(2);
|
|
1811
1860
|
logger$8.info("Dependencies restored from cache (file)", {
|
|
1812
1861
|
sizeMB,
|
|
1813
|
-
targetDir:
|
|
1862
|
+
targetDir: workDir
|
|
1814
1863
|
});
|
|
1815
1864
|
return;
|
|
1816
1865
|
}
|
|
@@ -1824,19 +1873,9 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
1824
1873
|
try {
|
|
1825
1874
|
const { scratchDir, hash } = await extractIntoScratch(depsUrl, kiciDir, attempt);
|
|
1826
1875
|
if (depsHash && hash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${hash}`);
|
|
1827
|
-
await
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
recursive: true,
|
|
1831
|
-
force: true
|
|
1832
|
-
});
|
|
1833
|
-
} catch (cleanupErr) {
|
|
1834
|
-
logger$8.warn("Scratch dir cleanup failed (orphan left behind)", {
|
|
1835
|
-
scratchDir,
|
|
1836
|
-
error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
logger$8.info("Dependencies restored from cache (stream)", { targetDir: kiciDir });
|
|
1876
|
+
await moveScratchIntoRepo(scratchDir, workDir);
|
|
1877
|
+
await cleanupScratch(scratchDir);
|
|
1878
|
+
logger$8.info("Dependencies restored from cache (stream)", { targetDir: workDir });
|
|
1840
1879
|
return;
|
|
1841
1880
|
} catch (err) {
|
|
1842
1881
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -2704,46 +2743,208 @@ function redactNpmOutput(input, tokens) {
|
|
|
2704
2743
|
}
|
|
2705
2744
|
var init_npm_registry_config = __esmMin((() => {}));
|
|
2706
2745
|
//#endregion
|
|
2746
|
+
//#region src/execution/validate-kici-deps.ts
|
|
2747
|
+
/**
|
|
2748
|
+
* Pre-install validation for `.kici/` dependency specifiers.
|
|
2749
|
+
*
|
|
2750
|
+
* The agent clones a single source repository and installs its `.kici/`
|
|
2751
|
+
* dependencies with the repo's package manager. Local-protocol specifiers —
|
|
2752
|
+
* `workspace:`, `file:`, `link:`, `portal:` — resolve a dependency against
|
|
2753
|
+
* another package on the same filesystem rather than a registry. Whether they
|
|
2754
|
+
* are resolvable depends on the manager and the layout:
|
|
2755
|
+
*
|
|
2756
|
+
* - npm has no `workspace:` protocol and cannot resolve any of these from a
|
|
2757
|
+
* registry, so they are rejected up front with an actionable message
|
|
2758
|
+
* instead of the raw `EUNSUPPORTEDPROTOCOL` npm would emit.
|
|
2759
|
+
* - pnpm resolves `workspace:` against the repo's pnpm workspace (the agent
|
|
2760
|
+
* clones the whole repo, so an in-repo sibling is present), and resolves
|
|
2761
|
+
* `file:`/`link:`/`portal:` against a path — allowed when that path stays
|
|
2762
|
+
* inside the cloned repo, rejected when it escapes the clone.
|
|
2763
|
+
*
|
|
2764
|
+
* This module performs that classification so unresolvable specifiers fail
|
|
2765
|
+
* fast with guidance rather than a cryptic install error.
|
|
2766
|
+
*/
|
|
2767
|
+
/**
|
|
2768
|
+
* Scan a parsed `.kici/package.json` for dependency specifiers that use a
|
|
2769
|
+
* local protocol (`workspace:`/`file:`/`link:`/`portal:`). Returns one entry
|
|
2770
|
+
* per dependency, in field order. Returns an empty array when there are none.
|
|
2771
|
+
*/
|
|
2772
|
+
function findLocalProtocolDeps(pkg) {
|
|
2773
|
+
const found = [];
|
|
2774
|
+
for (const field of DEP_FIELDS) {
|
|
2775
|
+
const deps = pkg[field];
|
|
2776
|
+
if (!deps || typeof deps !== "object") continue;
|
|
2777
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
2778
|
+
if (typeof spec !== "string") continue;
|
|
2779
|
+
const protocol = LOCAL_PROTOCOLS.find((proto) => spec.startsWith(proto));
|
|
2780
|
+
if (protocol) found.push({
|
|
2781
|
+
name,
|
|
2782
|
+
spec,
|
|
2783
|
+
protocol
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
return found;
|
|
2788
|
+
}
|
|
2789
|
+
/** Parse `.kici/package.json`, returning `null` when it is missing or invalid. */
|
|
2790
|
+
async function readKiciPackageJson(kiciDir) {
|
|
2791
|
+
let raw;
|
|
2792
|
+
try {
|
|
2793
|
+
raw = await readFile(join(kiciDir, "package.json"), "utf-8");
|
|
2794
|
+
} catch {
|
|
2795
|
+
return null;
|
|
2796
|
+
}
|
|
2797
|
+
try {
|
|
2798
|
+
return JSON.parse(raw);
|
|
2799
|
+
} catch {
|
|
2800
|
+
return null;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Whether `.kici/package.json` declares any local-protocol dependency. Used to
|
|
2805
|
+
* decide whether the agent must build the in-repo workspace dependency closure
|
|
2806
|
+
* after a pnpm install (so a `workspace:` sibling's build output exists before
|
|
2807
|
+
* the workflow that imports it loads).
|
|
2808
|
+
*/
|
|
2809
|
+
async function kiciHasLocalProtocolDeps(kiciDir) {
|
|
2810
|
+
const pkg = await readKiciPackageJson(kiciDir);
|
|
2811
|
+
if (!pkg) return false;
|
|
2812
|
+
return findLocalProtocolDeps(pkg).length > 0;
|
|
2813
|
+
}
|
|
2814
|
+
async function fileExists$1(target) {
|
|
2815
|
+
try {
|
|
2816
|
+
await access(target);
|
|
2817
|
+
return true;
|
|
2818
|
+
} catch {
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
/** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
|
|
2823
|
+
function resolveLocalPath(kiciDir, dep) {
|
|
2824
|
+
const rawPath = dep.spec.slice(dep.protocol.length);
|
|
2825
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(kiciDir, rawPath);
|
|
2826
|
+
}
|
|
2827
|
+
/** Whether `target` is `repoRoot` itself or a path inside it. */
|
|
2828
|
+
function isInsideRepo(repoRoot, target) {
|
|
2829
|
+
const rel = relative(repoRoot, target);
|
|
2830
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Classify each local-protocol dependency for the detected package manager and
|
|
2834
|
+
* return the ones that are unresolvable in the agent's single-clone model.
|
|
2835
|
+
*/
|
|
2836
|
+
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
2837
|
+
if (packageManager === PackageManager.Npm) return [...deps];
|
|
2838
|
+
const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
|
|
2839
|
+
const unresolvable = [];
|
|
2840
|
+
for (const dep of deps) {
|
|
2841
|
+
if (dep.protocol === "workspace:") {
|
|
2842
|
+
if (!hasWorkspaceFile) unresolvable.push(dep);
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
|
|
2846
|
+
}
|
|
2847
|
+
return unresolvable;
|
|
2848
|
+
}
|
|
2849
|
+
/** Build the actionable error for unresolvable local-protocol dependencies. */
|
|
2850
|
+
function formatUnresolvableDepError(offenders, packageManager) {
|
|
2851
|
+
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
2852
|
+
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.`;
|
|
2853
|
+
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.`;
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Throw an actionable error when `.kici/package.json` declares a local-protocol
|
|
2857
|
+
* dependency the detected package manager cannot resolve from the single cloned
|
|
2858
|
+
* repository. A missing or unparseable package.json is left for the install to
|
|
2859
|
+
* report.
|
|
2860
|
+
*/
|
|
2861
|
+
async function assertResolvableDeps(args) {
|
|
2862
|
+
const pkg = await readKiciPackageJson(args.kiciDir);
|
|
2863
|
+
if (!pkg) return;
|
|
2864
|
+
const localDeps = findLocalProtocolDeps(pkg);
|
|
2865
|
+
if (localDeps.length === 0) return;
|
|
2866
|
+
const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
|
|
2867
|
+
if (offenders.length === 0) return;
|
|
2868
|
+
throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
|
|
2869
|
+
}
|
|
2870
|
+
var LOCAL_PROTOCOLS, DEP_FIELDS;
|
|
2871
|
+
var init_validate_kici_deps = __esmMin((() => {
|
|
2872
|
+
LOCAL_PROTOCOLS = [
|
|
2873
|
+
"workspace:",
|
|
2874
|
+
"file:",
|
|
2875
|
+
"link:",
|
|
2876
|
+
"portal:"
|
|
2877
|
+
];
|
|
2878
|
+
DEP_FIELDS = [
|
|
2879
|
+
"dependencies",
|
|
2880
|
+
"devDependencies",
|
|
2881
|
+
"optionalDependencies",
|
|
2882
|
+
"peerDependencies"
|
|
2883
|
+
];
|
|
2884
|
+
}));
|
|
2885
|
+
//#endregion
|
|
2707
2886
|
//#region src/execution/dep-installer.ts
|
|
2708
2887
|
/**
|
|
2709
2888
|
* Inline dependency installation for graceful degradation.
|
|
2710
2889
|
*
|
|
2711
|
-
* When dep cache is unavailable or download fails, the agent
|
|
2712
|
-
*
|
|
2890
|
+
* When the dep cache is unavailable or a download fails, the agent installs
|
|
2891
|
+
* `.kici/` dependencies directly with the repository's package manager.
|
|
2713
2892
|
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
2893
|
+
* The package manager is detected from the cloned repo (npm / pnpm); the
|
|
2894
|
+
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
2895
|
+
* is the default and ships with every Node.js install; pnpm is used when the
|
|
2896
|
+
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
2897
|
+
* `workspace:` siblings. yarn is detected but not yet supported and is
|
|
2898
|
+
* rejected with an actionable error.
|
|
2716
2899
|
*
|
|
2717
|
-
* Security:
|
|
2718
|
-
* cache poisoning across build jobs
|
|
2719
|
-
* taint the cache used by subsequent builds.
|
|
2720
|
-
*
|
|
2721
|
-
* `--ignore-scripts` whenever a private
|
|
2900
|
+
* Security: the install runs with an isolated per-invocation cache/store
|
|
2901
|
+
* directory to prevent cache poisoning across build jobs — a malicious
|
|
2902
|
+
* package.json in one repo cannot taint the cache used by subsequent builds.
|
|
2903
|
+
* The same pressure rules out letting lifecycle scripts see synthesized auth
|
|
2904
|
+
* env vars — the install runs with `--ignore-scripts` whenever a private
|
|
2905
|
+
* registry is configured.
|
|
2722
2906
|
*/
|
|
2723
2907
|
/**
|
|
2724
|
-
*
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
2908
|
+
* Detect the package manager for the cloned repo from its committed manifests.
|
|
2909
|
+
* A pnpm workspace's `packageManager` field + `pnpm-lock.yaml` live at the repo
|
|
2910
|
+
* root, so check there first; fall back to `.kici/` for a standalone
|
|
2911
|
+
* (non-workspace) project that carries its own lockfile; default to npm when
|
|
2912
|
+
* neither carries a signal. Uses the manifests-only detector so the agent's own
|
|
2913
|
+
* launch env (`npm_config_user_agent`) never leaks into the decision.
|
|
2914
|
+
*/
|
|
2915
|
+
async function detectKiciPackageManager(repoRoot, kiciDir) {
|
|
2916
|
+
return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Install `.kici/` dependencies inline with the repo's package manager.
|
|
2727
2920
|
*
|
|
2728
|
-
*
|
|
2729
|
-
*
|
|
2921
|
+
* Falls back to this when the dep cache is unavailable or a download fails.
|
|
2922
|
+
* The install runs with an isolated cache/store directory (created in
|
|
2923
|
+
* `os.tmpdir()`) to prevent cache poisoning between build jobs; the directory
|
|
2924
|
+
* is removed after installation.
|
|
2730
2925
|
*
|
|
2731
|
-
* If `opts.npmRegistries` / `opts.installEnvSecrets` is provided,
|
|
2732
|
-
*
|
|
2733
|
-
* the
|
|
2734
|
-
*
|
|
2735
|
-
* synthesized token env vars.
|
|
2926
|
+
* If `opts.npmRegistries` / `opts.installEnvSecrets` is provided, a job-scoped
|
|
2927
|
+
* `.kici/.npmrc` overlay is synthesized for the install, restored in `finally`,
|
|
2928
|
+
* and the install runs with `--ignore-scripts` so lifecycle scripts in a
|
|
2929
|
+
* committed `package.json` cannot exfiltrate the synthesized token env vars.
|
|
2736
2930
|
*
|
|
2737
|
-
* @param kiciDir - Path to the
|
|
2738
|
-
* @param opts - Optional registry / installEnv configuration.
|
|
2739
|
-
* behavior is identical to the pre-private-registry version.
|
|
2931
|
+
* @param kiciDir - Path to the `.kici/` directory containing package.json.
|
|
2932
|
+
* @param opts - Optional registry / installEnv / repoRoot configuration.
|
|
2740
2933
|
*/
|
|
2741
2934
|
async function installDeps(kiciDir, opts = {}) {
|
|
2935
|
+
const repoRoot = opts.repoRoot ?? dirname(kiciDir);
|
|
2936
|
+
const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
|
|
2742
2937
|
logger$5.info("Installing deps inline", {
|
|
2743
|
-
packageManager
|
|
2938
|
+
packageManager,
|
|
2744
2939
|
dir: kiciDir
|
|
2745
2940
|
});
|
|
2746
|
-
process.stderr.write(`[dep-installer:trace] starting install: pm
|
|
2941
|
+
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
2942
|
+
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.");
|
|
2943
|
+
await assertResolvableDeps({
|
|
2944
|
+
kiciDir,
|
|
2945
|
+
repoRoot,
|
|
2946
|
+
packageManager
|
|
2947
|
+
});
|
|
2747
2948
|
const startTime = Date.now();
|
|
2748
2949
|
const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
|
|
2749
2950
|
const registryConfig = await applyNpmRegistryConfig({
|
|
@@ -2753,125 +2954,312 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
2753
2954
|
jobIdShort: opts.jobIdShort ?? "00000000"
|
|
2754
2955
|
});
|
|
2755
2956
|
try {
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
const { promisify } = await import("node:util");
|
|
2767
|
-
const execFileAsync = promisify(execFile);
|
|
2768
|
-
const buildArgs = (...prefix) => {
|
|
2769
|
-
const args = [
|
|
2770
|
-
...prefix,
|
|
2771
|
-
npmCmd,
|
|
2772
|
-
"--cache",
|
|
2773
|
-
cacheDir,
|
|
2774
|
-
"--no-audit",
|
|
2775
|
-
"--no-fund"
|
|
2776
|
-
];
|
|
2777
|
-
if (hasPrivateRegistry) args.push("--ignore-scripts");
|
|
2778
|
-
return args;
|
|
2779
|
-
};
|
|
2780
|
-
try {
|
|
2781
|
-
if (npmCliPath) {
|
|
2782
|
-
process.stderr.write(`[dep-installer:trace] running: ${nodeExe} ${npmCliPath} ${buildArgs().join(" ")}\n`);
|
|
2783
|
-
await execFileAsync(nodeExe, buildArgs(npmCliPath), {
|
|
2784
|
-
cwd: kiciDir,
|
|
2785
|
-
env: envWithNode,
|
|
2786
|
-
timeout: 6e5,
|
|
2787
|
-
maxBuffer: 128 * 1024 * 1024
|
|
2788
|
-
});
|
|
2789
|
-
} else {
|
|
2790
|
-
process.stderr.write(`[dep-installer:trace] running: npm ${buildArgs().join(" ")}\n`);
|
|
2791
|
-
await execFileAsync("npm", buildArgs(), {
|
|
2792
|
-
cwd: kiciDir,
|
|
2793
|
-
env: envWithNode,
|
|
2794
|
-
timeout: 6e5,
|
|
2795
|
-
maxBuffer: 128 * 1024 * 1024
|
|
2796
|
-
});
|
|
2797
|
-
}
|
|
2798
|
-
} finally {
|
|
2799
|
-
await rm(cacheDir, {
|
|
2800
|
-
recursive: true,
|
|
2801
|
-
force: true
|
|
2802
|
-
}).catch(() => {});
|
|
2803
|
-
}
|
|
2957
|
+
if (packageManager === PackageManager.Pnpm) await runPnpmInstall({
|
|
2958
|
+
kiciDir,
|
|
2959
|
+
hasPrivateRegistry,
|
|
2960
|
+
registryConfig
|
|
2961
|
+
});
|
|
2962
|
+
else await runNpmInstall({
|
|
2963
|
+
kiciDir,
|
|
2964
|
+
hasPrivateRegistry,
|
|
2965
|
+
registryConfig
|
|
2966
|
+
});
|
|
2804
2967
|
} catch (e) {
|
|
2805
|
-
const msg = toErrorMessage(e);
|
|
2806
2968
|
const tokens = registryConfig.tokensForRedaction;
|
|
2807
|
-
process.stderr.write(`[dep-installer:trace] INSTALL FAILED: ${redactNpmOutput(
|
|
2808
|
-
|
|
2809
|
-
if (e && typeof e === "object" && "stderr" in e) process.stderr.write(`[dep-installer:trace] stderr: ${redactNpmOutput(String(e.stderr), tokens).slice(0, 500)}\n`);
|
|
2969
|
+
process.stderr.write(`[dep-installer:trace] INSTALL FAILED: ${redactNpmOutput(toErrorMessage(e), tokens)}\n`);
|
|
2970
|
+
logSubprocessStreams(e, tokens);
|
|
2810
2971
|
throw e;
|
|
2811
2972
|
} finally {
|
|
2812
2973
|
await registryConfig.cleanup();
|
|
2813
2974
|
}
|
|
2975
|
+
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
2814
2976
|
const durationMs = Date.now() - startTime;
|
|
2815
2977
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
2816
2978
|
logger$5.info("Deps installed inline", {
|
|
2817
|
-
packageManager
|
|
2979
|
+
packageManager,
|
|
2818
2980
|
durationMs
|
|
2819
2981
|
});
|
|
2820
2982
|
}
|
|
2821
|
-
|
|
2983
|
+
/** Build the Node binary directory onto PATH so spawned tools find `node`. */
|
|
2984
|
+
function envWithNodeOnPath(extraEnv, nodeDir) {
|
|
2985
|
+
const { NODE_ENV: _NODE_ENV, ...restEnv } = process.env;
|
|
2986
|
+
return {
|
|
2987
|
+
...restEnv,
|
|
2988
|
+
...extraEnv,
|
|
2989
|
+
PATH: `${nodeDir}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
/** Run `npm install` in `.kici/` with an isolated cache directory. */
|
|
2993
|
+
async function runNpmInstall(args) {
|
|
2994
|
+
const { npmCliPath, nodeExe, nodeDir } = resolveNpm();
|
|
2995
|
+
const cacheDir = await mkdtemp(join(tmpdir(), "kici-npm-cache-"));
|
|
2996
|
+
const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
|
|
2997
|
+
const buildArgs = (...prefix) => {
|
|
2998
|
+
const a = [
|
|
2999
|
+
...prefix,
|
|
3000
|
+
"install",
|
|
3001
|
+
"--cache",
|
|
3002
|
+
cacheDir,
|
|
3003
|
+
"--no-audit",
|
|
3004
|
+
"--no-fund"
|
|
3005
|
+
];
|
|
3006
|
+
if (args.hasPrivateRegistry) a.push("--ignore-scripts");
|
|
3007
|
+
return a;
|
|
3008
|
+
};
|
|
3009
|
+
try {
|
|
3010
|
+
const bin = npmCliPath ? nodeExe : "npm";
|
|
3011
|
+
const argv = npmCliPath ? buildArgs(npmCliPath) : buildArgs();
|
|
3012
|
+
process.stderr.write(`[dep-installer:trace] running: ${bin} ${argv.join(" ")}\n`);
|
|
3013
|
+
await execFileAsync(bin, argv, {
|
|
3014
|
+
cwd: args.kiciDir,
|
|
3015
|
+
env,
|
|
3016
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
3017
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
3018
|
+
});
|
|
3019
|
+
} finally {
|
|
3020
|
+
await rm(cacheDir, {
|
|
3021
|
+
recursive: true,
|
|
3022
|
+
force: true
|
|
3023
|
+
}).catch(() => {});
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Run `pnpm install` from `.kici/`. pnpm walks up to the workspace root, so a
|
|
3028
|
+
* `workspace:` sibling in the same cloned repo resolves. Uses an isolated store
|
|
3029
|
+
* (`--config.store-dir`) for cross-job isolation, `package-import-method=copy`
|
|
3030
|
+
* so the on-disk store is a self-contained tree of real files (a later dep
|
|
3031
|
+
* cache tars it), and disables interactive purge prompts + the side-effects
|
|
3032
|
+
* cache for deterministic, non-interactive runs.
|
|
3033
|
+
*/
|
|
3034
|
+
async function runPnpmInstall(args) {
|
|
3035
|
+
await assertPnpmAvailable();
|
|
3036
|
+
const { nodeDir } = resolveNpm();
|
|
3037
|
+
const storeDir = await mkdtemp(join(tmpdir(), "kici-pnpm-store-"));
|
|
3038
|
+
const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
|
|
3039
|
+
const argv = [
|
|
3040
|
+
"install",
|
|
3041
|
+
`--config.store-dir=${storeDir}`,
|
|
3042
|
+
"--config.package-import-method=copy",
|
|
3043
|
+
"--config.confirm-modules-purge=false",
|
|
3044
|
+
"--config.side-effects-cache=false"
|
|
3045
|
+
];
|
|
3046
|
+
if (args.hasPrivateRegistry) argv.push("--ignore-scripts");
|
|
3047
|
+
try {
|
|
3048
|
+
process.stderr.write(`[dep-installer:trace] running: pnpm ${argv.join(" ")}\n`);
|
|
3049
|
+
await execFileAsync("pnpm", argv, {
|
|
3050
|
+
cwd: args.kiciDir,
|
|
3051
|
+
env,
|
|
3052
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
3053
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
3054
|
+
});
|
|
3055
|
+
} finally {
|
|
3056
|
+
await rm(storeDir, {
|
|
3057
|
+
recursive: true,
|
|
3058
|
+
force: true
|
|
3059
|
+
}).catch(() => {});
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Build the in-repo dependency closure of the `.kici/` package so a
|
|
3064
|
+
* `workspace:` sibling's build output exists before the workflow that imports
|
|
3065
|
+
* it loads. `--filter "{.kici}^..."` selects only `.kici`'s dependencies (not
|
|
3066
|
+
* `.kici` itself); `run --if-present build` skips siblings without a build
|
|
3067
|
+
* script. The `--if-present` flag MUST precede the script name: pnpm forwards
|
|
3068
|
+
* everything after the script name to the script itself, so `run build
|
|
3069
|
+
* --if-present` would pass `--if-present` to the build command (e.g. `tsc`)
|
|
3070
|
+
* and fail. Runs with a clean env (no synthesized registry tokens). On failure
|
|
3071
|
+
* the subprocess stderr/stdout is folded into the thrown error so the job's
|
|
3072
|
+
* failure message names the real cause instead of a bare "Command failed".
|
|
3073
|
+
*/
|
|
3074
|
+
async function buildWorkspaceClosure(repoRoot) {
|
|
3075
|
+
const { nodeDir } = resolveNpm();
|
|
3076
|
+
const env = envWithNodeOnPath({}, nodeDir);
|
|
3077
|
+
const argv = [
|
|
3078
|
+
"--filter",
|
|
3079
|
+
"{.kici}^...",
|
|
3080
|
+
"run",
|
|
3081
|
+
"--if-present",
|
|
3082
|
+
"build"
|
|
3083
|
+
];
|
|
3084
|
+
process.stderr.write(`[dep-installer:trace] building workspace closure: pnpm ${argv.join(" ")}\n`);
|
|
3085
|
+
try {
|
|
3086
|
+
await execFileAsync("pnpm", argv, {
|
|
3087
|
+
cwd: repoRoot,
|
|
3088
|
+
env,
|
|
3089
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
3090
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
3091
|
+
});
|
|
3092
|
+
} catch (e) {
|
|
3093
|
+
logSubprocessStreams(e, []);
|
|
3094
|
+
throw new Error(`Failed to build .kici workspace dependency closure: ${describeExecError(e)}`);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
/** Build a single-line cause from an exec error's stderr/stdout, falling back to its message. */
|
|
3098
|
+
function describeExecError(e) {
|
|
3099
|
+
const pick = (k) => e && typeof e === "object" && k in e ? String(e[k]).trim() : "";
|
|
3100
|
+
return (pick("stderr") || pick("stdout") || toErrorMessage(e)).slice(0, 2e3);
|
|
3101
|
+
}
|
|
3102
|
+
/** Throw an actionable error when the repo needs pnpm but it is not installed. */
|
|
3103
|
+
async function assertPnpmAvailable() {
|
|
3104
|
+
try {
|
|
3105
|
+
await execFileAsync("pnpm", ["--version"], { timeout: 3e4 });
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
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)})`);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
/** Trace the redacted stdout/stderr of a failed install subprocess. */
|
|
3111
|
+
function logSubprocessStreams(e, tokens) {
|
|
3112
|
+
if (e && typeof e === "object" && "stdout" in e) process.stderr.write(`[dep-installer:trace] stdout: ${redactNpmOutput(String(e.stdout), tokens).slice(0, 500)}\n`);
|
|
3113
|
+
if (e && typeof e === "object" && "stderr" in e) process.stderr.write(`[dep-installer:trace] stderr: ${redactNpmOutput(String(e.stderr), tokens).slice(0, 500)}\n`);
|
|
3114
|
+
}
|
|
3115
|
+
var logger$5, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
|
|
2822
3116
|
var init_dep_installer = __esmMin((() => {
|
|
2823
3117
|
init_npm_resolver();
|
|
2824
3118
|
init_npm_registry_config();
|
|
3119
|
+
init_validate_kici_deps();
|
|
2825
3120
|
logger$5 = createLogger({ prefix: "dep-installer" });
|
|
3121
|
+
execFileAsync = promisify(execFile);
|
|
3122
|
+
INSTALL_TIMEOUT_MS = 6e5;
|
|
3123
|
+
INSTALL_MAX_BUFFER = 128 * 1024 * 1024;
|
|
2826
3124
|
}));
|
|
2827
3125
|
//#endregion
|
|
2828
3126
|
//#region src/execution/dep-packer.ts
|
|
2829
3127
|
/**
|
|
2830
|
-
*
|
|
3128
|
+
* Dependency tarball creation for build agents.
|
|
3129
|
+
*
|
|
3130
|
+
* After installing dependencies the build agent packs the dependency closure
|
|
3131
|
+
* into a gzip tarball for upload to the dep cache. The closure is packed
|
|
3132
|
+
* **repo-root-relative** (cwd = the clone root) so restore is a single layout
|
|
3133
|
+
* regardless of package manager:
|
|
2831
3134
|
*
|
|
2832
|
-
*
|
|
2833
|
-
*
|
|
3135
|
+
* - npm / yarn: just `.kici/node_modules`.
|
|
3136
|
+
* - pnpm: `.kici/node_modules` plus the repo-root `node_modules/.pnpm` virtual
|
|
3137
|
+
* store and the in-repo `workspace:` sibling package directories `.kici`
|
|
3138
|
+
* depends on (with their built output). pnpm lays `.kici/node_modules` out as
|
|
3139
|
+
* symlinks into the root store and into sibling dirs that live outside
|
|
3140
|
+
* `.kici/`, so packing `.kici/node_modules` alone would capture dangling
|
|
3141
|
+
* links — the store and siblings must travel together.
|
|
2834
3142
|
*
|
|
2835
|
-
* Uses tar.gz
|
|
2836
|
-
*
|
|
3143
|
+
* Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
|
|
3144
|
+
* strip user/group info for cross-machine consistency; symlinks are preserved
|
|
3145
|
+
* as symlinks so the pnpm link graph restores intact.
|
|
2837
3146
|
*/
|
|
2838
3147
|
/**
|
|
2839
|
-
* Pack
|
|
3148
|
+
* Pack the dependency closure into a gzip tarball and compute its SHA-256 hash.
|
|
2840
3149
|
*
|
|
2841
|
-
*
|
|
2842
|
-
*
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
* @param kiciDir - Path to the .kici/ directory containing node_modules/
|
|
2846
|
-
* @returns Object with tarball Buffer and SHA-256 hash string
|
|
2847
|
-
* @throws Error if node_modules/ does not exist in kiciDir
|
|
3150
|
+
* @param kiciDir - Path to the `.kici/` directory containing node_modules/.
|
|
3151
|
+
* @returns Object with the tarball Buffer and its SHA-256 hash string.
|
|
3152
|
+
* @throws Error if `.kici/node_modules` does not exist.
|
|
2848
3153
|
*/
|
|
2849
3154
|
async function packNodeModules(kiciDir) {
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
3155
|
+
if (!existsSync(join(kiciDir, "node_modules"))) throw new Error(`node_modules not found at ${join(kiciDir, "node_modules")}`);
|
|
3156
|
+
const workDir = dirname(kiciDir);
|
|
3157
|
+
const packageManager = await detectPackageManagerFromManifests(workDir) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
|
|
3158
|
+
const entries = await closureEntries(workDir, kiciDir, packageManager);
|
|
3159
|
+
logger$4.info("Packing dependency closure into tarball", {
|
|
3160
|
+
dir: workDir,
|
|
3161
|
+
packageManager,
|
|
3162
|
+
entries
|
|
3163
|
+
});
|
|
2853
3164
|
const startTime = Date.now();
|
|
2854
3165
|
const stream = c({
|
|
2855
3166
|
gzip: true,
|
|
2856
|
-
cwd:
|
|
3167
|
+
cwd: workDir,
|
|
2857
3168
|
portable: true
|
|
2858
|
-
},
|
|
3169
|
+
}, entries);
|
|
2859
3170
|
const chunks = [];
|
|
2860
3171
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
2861
3172
|
const tarball = Buffer.concat(chunks);
|
|
2862
3173
|
const hash = sha256(tarball);
|
|
2863
3174
|
const sizeMB = (tarball.length / (1024 * 1024)).toFixed(2);
|
|
2864
|
-
|
|
2865
|
-
logger$4.info("Node_modules packed", {
|
|
3175
|
+
logger$4.info("Dependency closure packed", {
|
|
2866
3176
|
sizeMB,
|
|
2867
3177
|
hash: hash.slice(0, 12),
|
|
2868
|
-
durationMs
|
|
3178
|
+
durationMs: Date.now() - startTime
|
|
2869
3179
|
});
|
|
2870
3180
|
return {
|
|
2871
3181
|
tarball,
|
|
2872
3182
|
hash
|
|
2873
3183
|
};
|
|
2874
3184
|
}
|
|
3185
|
+
/**
|
|
3186
|
+
* Compute the repo-root-relative tar entries for the dependency closure. npm /
|
|
3187
|
+
* yarn need only `.kici/node_modules`; pnpm additionally needs the root store
|
|
3188
|
+
* and the in-repo workspace siblings `.kici` resolves.
|
|
3189
|
+
*/
|
|
3190
|
+
async function closureEntries(workDir, kiciDir, packageManager) {
|
|
3191
|
+
const kiciNodeModules = relative(workDir, join(kiciDir, "node_modules"));
|
|
3192
|
+
if (packageManager !== PackageManager.Pnpm) return [kiciNodeModules];
|
|
3193
|
+
const entries = [kiciNodeModules];
|
|
3194
|
+
if (existsSync(join(workDir, "node_modules", ".pnpm"))) entries.push(join("node_modules", ".pnpm"));
|
|
3195
|
+
for (const sibling of await collectInRepoSiblings(workDir, kiciDir)) entries.push(sibling);
|
|
3196
|
+
return entries;
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Walk `.kici`'s `node_modules` (and transitively each in-repo sibling's
|
|
3200
|
+
* `node_modules`) collecting the repo-relative directories of `workspace:`
|
|
3201
|
+
* siblings — package dirs that live inside the clone but outside `.kici/` and
|
|
3202
|
+
* outside the root `node_modules/` store. Returns each dir once.
|
|
3203
|
+
*/
|
|
3204
|
+
async function collectInRepoSiblings(workDir, kiciDir) {
|
|
3205
|
+
const repoRoot = resolve(workDir);
|
|
3206
|
+
const kiciResolved = resolve(kiciDir);
|
|
3207
|
+
const rootNodeModules = resolve(join(workDir, "node_modules"));
|
|
3208
|
+
const found = /* @__PURE__ */ new Set();
|
|
3209
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3210
|
+
const queue = [join(kiciDir, "node_modules")];
|
|
3211
|
+
while (queue.length > 0) {
|
|
3212
|
+
const nmDir = queue.shift();
|
|
3213
|
+
const real = await realpath(nmDir).catch(() => null);
|
|
3214
|
+
if (!real || visited.has(real)) continue;
|
|
3215
|
+
visited.add(real);
|
|
3216
|
+
for (const target of await resolveNodeModulesLinks(nmDir)) {
|
|
3217
|
+
if (!isInside(repoRoot, target)) continue;
|
|
3218
|
+
if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
|
|
3219
|
+
const rel = relative(workDir, target);
|
|
3220
|
+
if (!found.has(rel)) {
|
|
3221
|
+
found.add(rel);
|
|
3222
|
+
queue.push(join(target, "node_modules"));
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
return [...found];
|
|
3227
|
+
}
|
|
3228
|
+
/** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
|
|
3229
|
+
async function resolveNodeModulesLinks(nmDir) {
|
|
3230
|
+
const targets = [];
|
|
3231
|
+
for (const entry of await readdir(nmDir).catch(() => [])) {
|
|
3232
|
+
if (entry.startsWith(".")) continue;
|
|
3233
|
+
const entryPath = join(nmDir, entry);
|
|
3234
|
+
if (entry.startsWith("@")) {
|
|
3235
|
+
for (const scoped of await readdir(entryPath).catch(() => [])) {
|
|
3236
|
+
const target = await resolveIfSymlink(join(entryPath, scoped));
|
|
3237
|
+
if (target) targets.push(target);
|
|
3238
|
+
}
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
const target = await resolveIfSymlink(entryPath);
|
|
3242
|
+
if (target) targets.push(target);
|
|
3243
|
+
}
|
|
3244
|
+
return targets;
|
|
3245
|
+
}
|
|
3246
|
+
/** Return the real path of `p` if it is a symlink, else null. */
|
|
3247
|
+
async function resolveIfSymlink(p) {
|
|
3248
|
+
try {
|
|
3249
|
+
if (!(await lstat(p)).isSymbolicLink()) return null;
|
|
3250
|
+
return await realpath(p);
|
|
3251
|
+
} catch {
|
|
3252
|
+
return null;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
/** Whether `target` is `root` itself or a path inside it. */
|
|
3256
|
+
function isInside(root, target) {
|
|
3257
|
+
const rel = relative(root, target);
|
|
3258
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
|
|
3259
|
+
}
|
|
3260
|
+
function isAbsoluteRel(rel) {
|
|
3261
|
+
return rel.length > 1 && rel[1] === ":";
|
|
3262
|
+
}
|
|
2875
3263
|
var logger$4;
|
|
2876
3264
|
var init_dep_packer = __esmMin((() => {
|
|
2877
3265
|
logger$4 = createLogger({ prefix: "dep-packer" });
|
|
@@ -5053,14 +5441,14 @@ var init_job_runner = __esmMin((() => {
|
|
|
5053
5441
|
*/
|
|
5054
5442
|
init_console_capture();
|
|
5055
5443
|
init_npm_resolver();
|
|
5056
|
-
const AGENT_VERSION = "0.1.
|
|
5057
|
-
const BUILD_COMMIT = "
|
|
5058
|
-
const SDK_VERSION = "0.1.
|
|
5444
|
+
const AGENT_VERSION = "0.1.12";
|
|
5445
|
+
const BUILD_COMMIT = "d220ac45c";
|
|
5446
|
+
const SDK_VERSION = "0.1.12";
|
|
5059
5447
|
const SDK_BUNDLE_HASH = "675aafe4de03e677785ef2794d8f34951b14e2bd1e6f0ea825953d6c2abfe128";
|
|
5060
|
-
const SHARED_VERSION = "0.1.
|
|
5448
|
+
const SHARED_VERSION = "0.1.12";
|
|
5061
5449
|
const SHARED_BUNDLE_HASH = "36a23a454fd75ec35f258d39f254543ec0451c6b543fe3618ce427e6e6369aae";
|
|
5062
|
-
const ENGINE_VERSION = "0.1.
|
|
5063
|
-
const ENGINE_BUNDLE_HASH = "
|
|
5450
|
+
const ENGINE_VERSION = "0.1.12";
|
|
5451
|
+
const ENGINE_BUNDLE_HASH = "4ab08e63bcb8558b1191c5d3ab8d58b71950b77d911af56c9955ae1d787c4302";
|
|
5064
5452
|
initTelemetry({
|
|
5065
5453
|
serviceName: "kici-agent",
|
|
5066
5454
|
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|