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
265 lines
8.9 KiB
JavaScript
265 lines
8.9 KiB
JavaScript
/**
|
|
* Shared, zero-dependency discovery seam for the repo's Bun/Node workspaces and
|
|
* git submodules. This is the single source of truth the script layer reads to
|
|
* answer "which directories are workspaces?", "what package lives where?", and
|
|
* "which submodules does .gitmodules declare?" — replacing the ad-hoc glob
|
|
* walkers each script used to carry (the migration of those callers is #12333).
|
|
*
|
|
* The glob expander implements the npm/Bun `workspaces` semantics used by the
|
|
* root package.json: `*` matches a single path segment, `**` matches any number
|
|
* of segments, and a leading `!` pattern subtracts from earlier matches
|
|
* (exclude-wins, last-match-wins ordering). It is deliberately dependency-free —
|
|
* only node builtins plus reading package.json / .gitmodules — so any script or
|
|
* test can import it without pulling in the build graph.
|
|
*/
|
|
|
|
import {
|
|
existsSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
statSync,
|
|
} from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const DEFAULT_REPO_ROOT = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
"../../..",
|
|
);
|
|
|
|
function normalizePath(value) {
|
|
return value.split(path.sep).join("/");
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
}
|
|
|
|
function resolveRepoRoot(opts) {
|
|
return opts?.repoRoot ? path.resolve(opts.repoRoot) : DEFAULT_REPO_ROOT;
|
|
}
|
|
|
|
// Compile one workspace glob segment-pattern to a RegExp over the "/"-joined
|
|
// relative path. `**` spans segments (including zero), `*` stays within one.
|
|
function workspaceGlobToRegExp(glob) {
|
|
let pattern = "";
|
|
for (let i = 0; i < glob.length; i += 1) {
|
|
const char = glob[i];
|
|
// A `/**` suffix (or `/**/`) must match zero-or-more trailing segments,
|
|
// including none — so `packages/**` matches `packages` itself. Consume the
|
|
// preceding `/` with the `**` so the separator is optional too.
|
|
if (char === "/" && glob[i + 1] === "*" && glob[i + 2] === "*") {
|
|
if (glob[i + 3] === "/") {
|
|
pattern += "(?:/.*)?/";
|
|
i += 3;
|
|
} else {
|
|
pattern += "(?:/.*)?";
|
|
i += 2;
|
|
}
|
|
} else if (char === "*") {
|
|
if (glob[i + 1] === "*") {
|
|
pattern += ".*";
|
|
i += 1;
|
|
} else {
|
|
pattern += "[^/]*";
|
|
}
|
|
} else if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
pattern += `\\${char}`;
|
|
} else {
|
|
pattern += char;
|
|
}
|
|
}
|
|
return new RegExp(`^${pattern}$`);
|
|
}
|
|
|
|
// Hidden and heavy build/vendor dir names that are never workspace members and
|
|
// never worth descending into. Bun/npm workspace resolution likewise ignores
|
|
// dotfile dirs, and these build outputs (e.g. a Next.js `.next/package.json`
|
|
// marker) are not real packages — so a `*` segment must skip them just as a
|
|
// `**` walk does, or a stray `.next` gets matched as a workspace.
|
|
const WALK_SKIP_DIRS = new Set([
|
|
".git",
|
|
".next",
|
|
".turbo",
|
|
"build",
|
|
"coverage",
|
|
"dist",
|
|
"node_modules",
|
|
"storybook-static",
|
|
]);
|
|
|
|
// A directory child is traversable/matchable as a workspace only if it is not
|
|
// hidden and not a build/vendor dir. Applied uniformly to `*` and `**` so both
|
|
// segment kinds agree on membership.
|
|
function isTraversableChild(name) {
|
|
return !name.startsWith(".") && !WALK_SKIP_DIRS.has(name);
|
|
}
|
|
|
|
// Walk the tree expanding one positive glob into concrete directories. A `*`
|
|
// segment enumerates children; a `**` segment matches this directory and every
|
|
// descendant directory; a literal segment descends by name.
|
|
function expandPositiveGlob(repoRoot, pattern) {
|
|
let dirs = [repoRoot];
|
|
const parts = pattern.split("/");
|
|
for (let i = 0; i < parts.length; i += 1) {
|
|
const part = parts[i];
|
|
const next = [];
|
|
for (const dir of dirs) {
|
|
if (part === "**") {
|
|
// Match zero-or-more segments: keep this dir and add all descendants.
|
|
for (const descendant of walkDirs(dir)) next.push(descendant);
|
|
continue;
|
|
}
|
|
if (part === "*") {
|
|
for (const entry of readDirEntries(dir)) {
|
|
if (entry.isDirectory() && isTraversableChild(entry.name)) {
|
|
next.push(path.join(dir, entry.name));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
const candidate = path.join(dir, part);
|
|
try {
|
|
if (statSync(candidate).isDirectory()) next.push(candidate);
|
|
} catch {
|
|
// error-policy:J3 path does not exist — this glob branch yields nothing
|
|
}
|
|
}
|
|
dirs = next;
|
|
}
|
|
return dirs;
|
|
}
|
|
|
|
function readDirEntries(dir) {
|
|
try {
|
|
return readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
// error-policy:J3 unreadable dir contributes no members
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Depth-first directory list rooted at `start` (inclusive), skipping hidden and
|
|
// heavy build/vendor dirs so `**` expansion stays bounded and deterministic.
|
|
function walkDirs(start) {
|
|
const out = [start];
|
|
for (const entry of readDirEntries(start)) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (!isTraversableChild(entry.name)) continue;
|
|
out.push(...walkDirs(path.join(start, entry.name)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Expand a list of workspace glob patterns (npm/Bun `workspaces` semantics)
|
|
* into a deduped, sorted list of relative directory paths. Positive patterns
|
|
* add matches; a leading `!` pattern removes earlier matches (exclude-wins).
|
|
*/
|
|
export function expandWorkspaceGlobs(patterns, opts) {
|
|
const repoRoot = resolveRepoRoot(opts);
|
|
const matchers = patterns.map((glob) => {
|
|
const negated = glob.startsWith("!");
|
|
return {
|
|
negated,
|
|
regExp: workspaceGlobToRegExp(negated ? glob.slice(1) : glob),
|
|
};
|
|
});
|
|
|
|
function isMember(relativeDir) {
|
|
let member = false;
|
|
for (const { negated, regExp } of matchers) {
|
|
if (regExp.test(relativeDir)) member = !negated;
|
|
}
|
|
return member;
|
|
}
|
|
|
|
const dirs = new Set();
|
|
for (const glob of patterns) {
|
|
if (glob.startsWith("!")) continue;
|
|
for (const dir of expandPositiveGlob(repoRoot, glob)) {
|
|
const relativeDir = normalizePath(path.relative(repoRoot, dir));
|
|
if (!relativeDir || !isMember(relativeDir)) continue;
|
|
dirs.add(relativeDir);
|
|
}
|
|
}
|
|
return [...dirs].sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
/**
|
|
* List every workspace directory declared in the root package.json — expanded
|
|
* from the `workspaces` globs, negations applied, keeping only directories that
|
|
* actually contain a package.json.
|
|
*/
|
|
export function listWorkspaceDirs(opts) {
|
|
const repoRoot = resolveRepoRoot(opts);
|
|
const rootPackage = readJson(path.join(repoRoot, "package.json"));
|
|
const patterns = rootPackage.workspaces ?? [];
|
|
return expandWorkspaceGlobs(patterns, { repoRoot }).filter((relativeDir) =>
|
|
existsSync(path.join(repoRoot, relativeDir, "package.json")),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* List every workspace as `{ name, dir, packageJson }`, where `name` is the
|
|
* package.json `name` field (may be undefined for a private, unnamed package)
|
|
* and `dir` is the workspace-relative directory.
|
|
*/
|
|
export function listPackages(opts) {
|
|
const repoRoot = resolveRepoRoot(opts);
|
|
return listWorkspaceDirs({ repoRoot }).map((dir) => {
|
|
const packageJson = readJson(path.join(repoRoot, dir, "package.json"));
|
|
return { name: packageJson.name, dir, packageJson };
|
|
});
|
|
}
|
|
|
|
// Minimal INI parser for .gitmodules: sections keyed by `[submodule "name"]`,
|
|
// with `path` / `url` / `branch` values. Indentation and comment lines (`#`,
|
|
// `;`) are ignored; unknown keys are dropped.
|
|
function parseGitmodules(text) {
|
|
const sections = [];
|
|
let current = null;
|
|
for (const rawLine of text.split("\n")) {
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
const sectionMatch = line.match(/^\[submodule\s+"(.+)"\]$/);
|
|
if (sectionMatch) {
|
|
current = { section: sectionMatch[1] };
|
|
sections.push(current);
|
|
continue;
|
|
}
|
|
if (!current) continue;
|
|
const kvMatch = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(.*)$/);
|
|
if (!kvMatch) continue;
|
|
current[kvMatch[1]] = kvMatch[2].trim();
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
/**
|
|
* Parse the root `.gitmodules` into `{ path, url, branch, initialized }` per
|
|
* submodule. `initialized` is true when the submodule's working tree is present
|
|
* on disk (a `.git` gitfile or non-empty checkout), false when only the gitlink
|
|
* placeholder exists. Returns an empty list when `.gitmodules` is absent.
|
|
*/
|
|
export function listSubmodules(opts) {
|
|
const repoRoot = resolveRepoRoot(opts);
|
|
const gitmodulesPath = path.join(repoRoot, ".gitmodules");
|
|
if (!existsSync(gitmodulesPath)) return [];
|
|
const sections = parseGitmodules(readFileSync(gitmodulesPath, "utf8"));
|
|
return sections
|
|
.filter((section) => typeof section.path === "string")
|
|
.map((section) => ({
|
|
path: section.path,
|
|
url: section.url,
|
|
branch: section.branch,
|
|
initialized: isSubmoduleInitialized(path.join(repoRoot, section.path)),
|
|
}));
|
|
}
|
|
|
|
function isSubmoduleInitialized(absPath) {
|
|
if (existsSync(path.join(absPath, ".git"))) return true;
|
|
const entries = readDirEntries(absPath);
|
|
return entries.length > 0;
|
|
}
|