Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

426 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Post-install script to initialize git submodules if they haven't been.
* This ensures tracked submodules from .gitmodules are initialized when
* cloning the repo or installing dependencies.
*
* Run automatically via the `postinstall` hook, or manually:
* node scripts/init-submodules.mjs
*/
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptFile = fileURLToPath(import.meta.url);
const __dirname = dirname(scriptFile);
const root = resolve(__dirname, "..");
const skipLocalUpstreams =
process.env.ELIZA_SKIP_LOCAL_UPSTREAMS === "1" ||
process.env.ELIZA_SKIP_LOCAL_UPSTREAMS === "1";
const SUBMODULE_READINESS_MARKERS = {
eliza: ["package.json", "packages/core/package.json"],
};
// plugin-openrouter contains PGlite :memory:<UUID> paths committed under
// typescript/ that Windows git rejects as invalid filenames. Skip checkout
// until elizaos-plugins/plugin-openrouter#25 is merged; the package is
// available via npm in the meantime.
const SKIP_SUBMODULES = new Set(["eliza/plugins/plugin-openrouter"]);
// Submodules whose own nested submodules should NOT be recursively initialized.
const NO_RECURSE_SUBMODULES = new Set([]);
/** Top-level paths that moved under `eliza/`; drop stale gitlinks after migration. */
const LEGACY_ROOT_SUBMODULE_PATHS = ["cloud", "steward-fi"];
function getSubmoduleSkipReason(
submodulePath,
{ skipLocal = skipLocalUpstreams } = {},
) {
if (SKIP_SUBMODULES.has(submodulePath)) {
return "it is in the explicit skip list";
}
if (skipLocal && submodulePath === "eliza") {
return "local upstreams are disabled";
}
return null;
}
export function shouldSkipSubmoduleInit(
submodulePath,
{ skipLocal = skipLocalUpstreams } = {},
) {
return getSubmoduleSkipReason(submodulePath, { skipLocal }) !== null;
}
export function parseTrackedSubmodules(configOutput) {
if (!configOutput.trim()) return [];
return configOutput
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [rawKey, path] = line.split(/\s+/, 2);
const name = rawKey.replace(/^submodule\./, "").replace(/\.path$/, "");
return { name, path };
});
}
export function loadTrackedSubmodules({ exec = execSync, cwd = root } = {}) {
try {
const output = exec(
'git config --file .gitmodules --get-regexp "^submodule\\..*\\.path$"',
{
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
},
).trim();
return parseTrackedSubmodules(output);
} catch {
return [];
}
}
/**
* After moving `cloud` and `steward-fi` under `eliza/`, older clones may still
* have gitlinks at the repo root. If `.gitmodules` no longer lists those paths
* but the index still does, remove them so postinstall does not clone into
* `./cloud` or `./steward-fi`.
*/
export function pruneLegacyRootSubmodulesMovedUnderEliza(
rootDir,
{ exec = execSync, log = console.log, logError = console.error } = {},
) {
const tracked = new Set(
loadTrackedSubmodules({ exec, cwd: rootDir }).map((s) => s.path),
);
for (const rel of LEGACY_ROOT_SUBMODULE_PATHS) {
if (tracked.has(rel)) {
continue;
}
let mode = "";
try {
const line = exec(`git ls-files -s -- "${rel}"`, {
cwd: rootDir,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (!line) {
continue;
}
mode = line.split(/\s+/)[0] ?? "";
} catch {
continue;
}
if (mode !== "160000") {
continue;
}
log(
`[init-submodules] Removing stale top-level submodule "${rel}" (now under eliza/). Deinitializing…`,
);
try {
exec(`git submodule deinit -f -- "${rel}"`, {
cwd: rootDir,
stdio: "inherit",
});
} catch {
// Best effort — worktree may already be missing.
}
try {
exec(`git rm -f -- "${rel}"`, {
cwd: rootDir,
stdio: "inherit",
});
} catch (err) {
logError(
`[init-submodules] Could not drop stale submodule "${rel}" from the index: ${
err instanceof Error ? err.message : String(err)
}`,
);
}
}
}
export function getSubmoduleReadinessMarkerPaths(
submodulePath,
{ rootDir = root } = {},
) {
const markers = SUBMODULE_READINESS_MARKERS[submodulePath] ?? [];
return markers.map((marker) => resolve(rootDir, submodulePath, marker));
}
export function isSubmoduleCheckoutReady(
submodulePath,
{ rootDir = root, exists = existsSync } = {},
) {
const markerPaths = getSubmoduleReadinessMarkerPaths(submodulePath, {
rootDir,
});
if (markerPaths.length === 0) {
return true;
}
return markerPaths.every((markerPath) => exists(markerPath));
}
export function isTrackedAsGitlink(
submodulePath,
{ exec = execSync, cwd = root } = {},
) {
try {
const output = exec(`git ls-files -s -- "${submodulePath}"`, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (!output) {
return false;
}
const lines = output.split("\n").filter(Boolean);
if (lines.length !== 1) {
return false;
}
const [mode, , , trackedPath] = lines[0].split(/\s+/, 4);
return mode === "160000" && trackedPath === submodulePath;
} catch {
return false;
}
}
export function runInitSubmodules({
rootDir = root,
exists = existsSync,
exec = execSync,
log = console.log,
logError = console.error,
shouldSkipSubmodule = shouldSkipSubmoduleInit,
} = {}) {
// Check if we're in a git repository
const gitDir = resolve(rootDir, ".git");
if (!exists(gitDir)) {
log("[init-submodules] Not a git repository — skipping");
return { initialized: 0, alreadyInitialized: 0, failed: 0, submodules: [] };
}
const gitmodulesPath = resolve(rootDir, ".gitmodules");
if (!exists(gitmodulesPath)) {
log("[init-submodules] No .gitmodules found — skipping");
return { initialized: 0, alreadyInitialized: 0, failed: 0, submodules: [] };
}
const submodules = loadTrackedSubmodules({ exec, cwd: rootDir });
if (submodules.length === 0) {
log("[init-submodules] No tracked submodules found — skipping");
return { initialized: 0, alreadyInitialized: 0, failed: 0, submodules: [] };
}
const hasLegacyRootCloudPaths = submodules.some(
(s) => s.path === "cloud" || s.path === "steward-fi",
);
if (hasLegacyRootCloudPaths) {
log(
"[init-submodules] This .gitmodules still lists cloud/ or steward-fi/ at the repo root. Pull the latest branch where those repos are nested under eliza/, or edit .gitmodules to match.",
);
}
pruneLegacyRootSubmodulesMovedUnderEliza(rootDir, {
exec,
log,
logError,
exists,
});
let initialized = 0;
let alreadyInitialized = 0;
let failed = 0;
for (const submodule of submodules) {
const skipReason = getSubmoduleSkipReason(submodule.path);
if (shouldSkipSubmodule(submodule.path)) {
log(
`[init-submodules] Skipping ${submodule.name} (${submodule.path}) because ${skipReason ?? "local upstreams are disabled"}`,
);
continue;
}
if (!isTrackedAsGitlink(submodule.path, { exec, cwd: rootDir })) {
log(
`[init-submodules] Skipping ${submodule.name} (${submodule.path}) because the parent repo tracks that path as regular files, not a gitlink`,
);
continue;
}
const checkoutReady = isSubmoduleCheckoutReady(submodule.path, {
rootDir,
exists,
});
let needsInit = !checkoutReady;
let initReason = checkoutReady ? "" : "checkout is partial";
try {
const status = exec(`git submodule status -- "${submodule.path}"`, {
cwd: rootDir,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (status.startsWith("-")) {
needsInit = true;
initReason = "submodule is not initialized";
} else if (status.startsWith("+")) {
// Submodule HEAD differs from the commit recorded in the parent
// index — local commits or a branch checkout exist.
log(
`[init-submodules] ⚠ ${submodule.name} (${submodule.path}) has commits not recorded in the parent repo`,
);
}
// Warn about uncommitted changes in initialized submodules.
if (!status.startsWith("-")) {
try {
const smRoot = resolve(rootDir, submodule.path);
const dirty = exec("git status --porcelain", {
cwd: smRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (dirty) {
log(
`[init-submodules] ⚠ ${submodule.name} (${submodule.path}) has uncommitted local changes`,
);
}
} catch {
// Cannot check — not critical, just skip the warning.
}
}
} catch {
// If status lookup fails, attempt initialization directly.
needsInit = true;
if (!initReason) {
initReason = "status check failed";
}
}
if (!needsInit) {
alreadyInitialized++;
continue;
}
log(
`[init-submodules] Initializing ${submodule.name} (${submodule.path})${
initReason ? ` because ${initReason}` : ""
}...`,
);
try {
const recurseFlag = NO_RECURSE_SUBMODULES.has(submodule.path)
? ""
: " --recursive";
try {
exec(`git submodule update --init${recurseFlag} "${submodule.path}"`, {
cwd: rootDir,
stdio: "inherit",
});
} catch (_shallowErr) {
// Shallow clones (common in CI) may fail to fetch the pinned SHA.
// Retry: register the submodule, fetch all refs deeply, then update.
log(
`[init-submodules] Shallow init failed for ${submodule.name}, retrying with full fetch...`,
);
try {
exec(`git submodule init "${submodule.path}"`, {
cwd: rootDir,
stdio: "inherit",
});
} catch {
// init may already have been done by the first attempt
}
const smRoot = resolve(rootDir, submodule.path);
if (exists(smRoot) && exists(resolve(smRoot, ".git"))) {
exec("git fetch --unshallow || git fetch --all", {
cwd: smRoot,
stdio: "inherit",
shell: true,
});
}
exec(`git submodule update${recurseFlag} "${submodule.path}"`, {
cwd: rootDir,
stdio: "inherit",
});
}
if (
!isSubmoduleCheckoutReady(submodule.path, {
rootDir,
exists,
})
) {
throw new Error(
`submodule checkout is still partial after update: ${submodule.path}`,
);
}
initialized++;
log(`[init-submodules] ${submodule.name} initialized successfully`);
} catch (err) {
failed++;
const message = err instanceof Error ? err.message : String(err);
logError(
`[init-submodules] Failed to initialize ${submodule.name} (${submodule.path}): ${message}`,
);
}
}
if (
!shouldSkipSubmodule("eliza") &&
exists(resolve(rootDir, "eliza", ".gitmodules"))
) {
log(
"[init-submodules] Ensuring nested checkouts under eliza/ (cloud, steward-fi, plugins, …)…",
);
try {
// Run from inside eliza/ so git reads eliza/.gitmodules directly.
// Running `git submodule update --init --recursive -- eliza` from the
// parent can fail when git tries to resolve nested submodule paths
// (e.g. eliza/cloud) against the parent's .gitmodules instead of
// eliza's own .gitmodules.
exec(`git submodule update --init --recursive`, {
cwd: resolve(rootDir, "eliza"),
stdio: "inherit",
});
} catch (err) {
logError(
`[init-submodules] Nested eliza submodule update failed (fix broken plugin submodules under eliza/ if needed): ${
err instanceof Error ? err.message : String(err)
}`,
);
}
}
if (failed > 0) {
logError(
`[init-submodules] Initialized ${initialized}, already ready ${alreadyInitialized}, failed ${failed}.`,
);
} else if (initialized === 0) {
log("[init-submodules] All submodules already initialized");
} else {
log(
`[init-submodules] Initialized ${initialized} submodule(s); ${alreadyInitialized} already ready.`,
);
}
return { initialized, alreadyInitialized, failed, submodules };
}
const isDirectRun =
process.argv[1] && resolve(process.argv[1]) === resolve(scriptFile);
if (isDirectRun) {
runInitSubmodules();
}