426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
1247 lines
35 KiB
TypeScript
1247 lines
35 KiB
TypeScript
/**
|
|
* Static action scanner for the agent-console example, extracting action
|
|
* metadata and subaction relationships from elizaOS package sources.
|
|
*/
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { dirname, join, normalize, relative, resolve, sep } from "node:path";
|
|
import ts from "typescript";
|
|
|
|
const SOURCE_ROOTS = ["packages", "plugins", "cloud"];
|
|
const EXCLUDE_RE =
|
|
/(^|\/)(dist|build|coverage|node_modules|\.turbo|\.next|generated)(\/|$)|(\.d\.ts$)|(\.(test|spec)\.tsx?$)|(^|\/)(__tests__|test|tests|templates)(\/|$)/;
|
|
|
|
type ImportedBinding = {
|
|
file: string;
|
|
imported: string;
|
|
};
|
|
|
|
type StaticBindings = {
|
|
stringBindings: Map<string, string>;
|
|
arrayBindings: Map<string, string[]>;
|
|
objectBindings: Map<string, ts.ObjectLiteralExpression>;
|
|
specBindings: Map<string, string>;
|
|
importBindings: Map<string, ImportedBinding>;
|
|
};
|
|
|
|
type ExportedStaticBindings = {
|
|
strings: Map<string, string>;
|
|
arrays: Map<string, string[]>;
|
|
};
|
|
|
|
export type ActionScanSort = "name" | "filepath";
|
|
|
|
type ScannedParameter = {
|
|
name: string;
|
|
description: string;
|
|
required?: boolean;
|
|
schemaType?: string;
|
|
enumValues: string[];
|
|
source: string;
|
|
};
|
|
|
|
type SubActionReference = {
|
|
raw: string;
|
|
name: string;
|
|
kind: "name" | "ref" | "inline" | "spread" | "expression";
|
|
found: boolean;
|
|
targetId?: string;
|
|
targetFile?: string;
|
|
targetLine?: number;
|
|
};
|
|
|
|
type InferredSubAction = {
|
|
name: string;
|
|
parameter: string;
|
|
source: "parameter-enum" | "parameter-description";
|
|
};
|
|
|
|
type ScannedAction = {
|
|
id: string;
|
|
name: string;
|
|
nameStatic: boolean;
|
|
declarationName: string;
|
|
actionType: string;
|
|
detectedBy: "shape" | "type";
|
|
source: string;
|
|
file: string;
|
|
absoluteFile: string;
|
|
line: number;
|
|
description: string;
|
|
contexts: string[];
|
|
tags: string[];
|
|
similes: string[];
|
|
mode: string;
|
|
roleGate: string;
|
|
contextGate: string;
|
|
validation: "always_true" | "conditional" | "missing";
|
|
subPlanner: boolean;
|
|
parameters: ScannedParameter[];
|
|
parameterSummary: string[];
|
|
subActions: string[];
|
|
resolvedSubActions: SubActionReference[];
|
|
inferredSubActions: InferredSubAction[];
|
|
parentIds: string[];
|
|
parentNames: string[];
|
|
};
|
|
|
|
type ActionTreeNode = {
|
|
id: string;
|
|
name: string;
|
|
kind: "action" | "missing" | "inferred";
|
|
file?: string;
|
|
absoluteFile?: string;
|
|
line?: number;
|
|
source?: string;
|
|
actionId?: string;
|
|
ref?: string;
|
|
parameter?: string;
|
|
inferredSource?: InferredSubAction["source"];
|
|
cycle?: boolean;
|
|
children: ActionTreeNode[];
|
|
};
|
|
|
|
type ActionScanResult = {
|
|
generatedAt: string;
|
|
repoRoot: string;
|
|
sourceRoots: string[];
|
|
filesScanned: number;
|
|
actionCount: number;
|
|
rootCount: number;
|
|
parentCount: number;
|
|
unresolvedSubActionCount: number;
|
|
inferredSubActionCount: number;
|
|
dynamicNameCount: number;
|
|
sort: ActionScanSort;
|
|
sourceGroups: Array<{ source: string; count: number }>;
|
|
roots: ActionTreeNode[];
|
|
actions: ScannedAction[];
|
|
};
|
|
|
|
const exportedBindingCache = new Map<string, ExportedStaticBindings>();
|
|
|
|
function toRepoPath(file: string): string {
|
|
return file.split(sep).join("/");
|
|
}
|
|
|
|
function hasExportModifier(node: ts.Node): boolean {
|
|
return Boolean(
|
|
ts.canHaveModifiers(node) &&
|
|
ts
|
|
.getModifiers(node)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword),
|
|
);
|
|
}
|
|
|
|
function unwrapExpression(
|
|
expr: ts.Expression | undefined,
|
|
): ts.Expression | undefined {
|
|
let current = expr;
|
|
while (
|
|
current &&
|
|
(ts.isAsExpression(current) ||
|
|
ts.isSatisfiesExpression(current) ||
|
|
ts.isParenthesizedExpression(current))
|
|
) {
|
|
current = current.expression;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function propName(prop: ts.ObjectLiteralElementLike): string | undefined {
|
|
const name = prop.name;
|
|
if (!name) return undefined;
|
|
if (
|
|
ts.isIdentifier(name) ||
|
|
ts.isStringLiteral(name) ||
|
|
ts.isNumericLiteral(name)
|
|
) {
|
|
return name.text;
|
|
}
|
|
return name.getText();
|
|
}
|
|
|
|
function getProp(
|
|
obj: ts.ObjectLiteralExpression,
|
|
names: string | string[],
|
|
): ts.ObjectLiteralElementLike | undefined {
|
|
const wanted = new Set(Array.isArray(names) ? names : [names]);
|
|
for (const prop of obj.properties) {
|
|
if (
|
|
(ts.isPropertyAssignment(prop) ||
|
|
ts.isMethodDeclaration(prop) ||
|
|
ts.isShorthandPropertyAssignment(prop)) &&
|
|
wanted.has(propName(prop) ?? "")
|
|
) {
|
|
return prop;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getPropExpression(
|
|
prop: ts.ObjectLiteralElementLike | undefined,
|
|
): ts.Expression | undefined {
|
|
if (!prop) return undefined;
|
|
if (ts.isPropertyAssignment(prop)) return prop.initializer;
|
|
if (ts.isShorthandPropertyAssignment(prop)) return prop.name;
|
|
return undefined;
|
|
}
|
|
|
|
function literalText(expr: ts.Expression | undefined): string | undefined {
|
|
const unwrapped = unwrapExpression(expr);
|
|
if (!unwrapped) return undefined;
|
|
if (ts.isStringLiteralLike(unwrapped)) return unwrapped.text;
|
|
if (unwrapped.kind === ts.SyntaxKind.TrueKeyword) return "true";
|
|
if (unwrapped.kind === ts.SyntaxKind.FalseKeyword) return "false";
|
|
if (ts.isNumericLiteral(unwrapped)) return unwrapped.text;
|
|
return undefined;
|
|
}
|
|
|
|
function exprText(
|
|
expr: ts.Node | undefined,
|
|
sf: ts.SourceFile,
|
|
max = 220,
|
|
): string {
|
|
if (!expr) return "";
|
|
const text = expr.getText(sf).replace(/\s+/g, " ").trim();
|
|
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
}
|
|
|
|
function propSource(
|
|
obj: ts.ObjectLiteralExpression,
|
|
names: string | string[],
|
|
sf: ts.SourceFile,
|
|
max = 220,
|
|
): string {
|
|
return exprText(getPropExpression(getProp(obj, names)), sf, max);
|
|
}
|
|
|
|
function readArrayFromExpression(
|
|
expr: ts.Expression | undefined,
|
|
sf: ts.SourceFile,
|
|
bindings?: StaticBindings | ExportedStaticBindings,
|
|
): string[] | undefined {
|
|
const unwrapped = unwrapExpression(expr);
|
|
if (!unwrapped) return undefined;
|
|
if (!ts.isArrayLiteralExpression(unwrapped)) return undefined;
|
|
|
|
const values = unwrapped.elements.flatMap((element): string[] => {
|
|
if (ts.isStringLiteralLike(element)) return [element.text];
|
|
if (ts.isIdentifier(element)) {
|
|
if (bindings && "stringBindings" in bindings) {
|
|
const stringValue = bindings.stringBindings.get(element.text);
|
|
if (stringValue) return [stringValue];
|
|
const arrayValue = bindings.arrayBindings.get(element.text);
|
|
if (arrayValue) return arrayValue;
|
|
} else if (bindings) {
|
|
const stringValue = bindings.strings.get(element.text);
|
|
if (stringValue) return [stringValue];
|
|
const arrayValue = bindings.arrays.get(element.text);
|
|
if (arrayValue) return arrayValue;
|
|
}
|
|
}
|
|
if (ts.isSpreadElement(element) && ts.isIdentifier(element.expression)) {
|
|
if (bindings && "arrayBindings" in bindings) {
|
|
return bindings.arrayBindings.get(element.expression.text) ?? [];
|
|
}
|
|
if (bindings && "arrays" in bindings) {
|
|
return bindings.arrays.get(element.expression.text) ?? [];
|
|
}
|
|
}
|
|
return [exprText(element, sf, 120)].filter(Boolean);
|
|
});
|
|
|
|
return values.length > 0 ? values : undefined;
|
|
}
|
|
|
|
function collectExportedStaticBindings(
|
|
repoRoot: string,
|
|
file: string,
|
|
): ExportedStaticBindings {
|
|
const cached = exportedBindingCache.get(file);
|
|
if (cached) return cached;
|
|
|
|
const result: ExportedStaticBindings = {
|
|
strings: new Map(),
|
|
arrays: new Map(),
|
|
};
|
|
exportedBindingCache.set(file, result);
|
|
|
|
const abs = join(repoRoot, file);
|
|
if (!existsSync(abs)) return result;
|
|
|
|
const source = readFileSync(abs, "utf8");
|
|
const sf = ts.createSourceFile(
|
|
file,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
);
|
|
|
|
function visit(node: ts.Node) {
|
|
if (ts.isVariableStatement(node) && hasExportModifier(node)) {
|
|
for (const declaration of node.declarationList.declarations) {
|
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
continue;
|
|
|
|
const initializer = unwrapExpression(declaration.initializer);
|
|
const text = literalText(initializer);
|
|
if (text !== undefined) {
|
|
result.strings.set(declaration.name.text, text);
|
|
continue;
|
|
}
|
|
|
|
const array = readArrayFromExpression(initializer, sf, result);
|
|
if (array) result.arrays.set(declaration.name.text, array);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sf);
|
|
return result;
|
|
}
|
|
|
|
function importedBindingValue(
|
|
repoRoot: string,
|
|
bindings: StaticBindings,
|
|
localName: string,
|
|
) {
|
|
const imported = bindings.importBindings.get(localName);
|
|
if (!imported) return undefined;
|
|
|
|
const exported = collectExportedStaticBindings(repoRoot, imported.file);
|
|
const stringValue = exported.strings.get(imported.imported);
|
|
if (stringValue) return { type: "string" as const, value: stringValue };
|
|
|
|
const arrayValue = exported.arrays.get(imported.imported);
|
|
if (arrayValue) return { type: "array" as const, value: arrayValue };
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function collectStaticBindings(
|
|
repoRoot: string,
|
|
sf: ts.SourceFile,
|
|
file: string,
|
|
): StaticBindings {
|
|
const stringBindings = new Map<string, string>();
|
|
const arrayBindings = new Map<string, string[]>();
|
|
const objectBindings = new Map<string, ts.ObjectLiteralExpression>();
|
|
const specBindings = new Map<string, string>();
|
|
const importBindings = new Map<string, ImportedBinding>();
|
|
|
|
function resolveImportPath(specifier: string): string | undefined {
|
|
if (!specifier.startsWith(".")) return undefined;
|
|
|
|
const fromDir = dirname(file);
|
|
const candidate = normalize(join(fromDir, specifier));
|
|
const candidates = [
|
|
candidate.endsWith(".js") ? candidate.replace(/\.js$/, ".ts") : "",
|
|
candidate.endsWith(".js") ? candidate.replace(/\.js$/, ".tsx") : "",
|
|
candidate,
|
|
`${candidate}.ts`,
|
|
`${candidate}.tsx`,
|
|
`${candidate}.js`,
|
|
join(candidate, "index.ts"),
|
|
join(candidate, "index.tsx"),
|
|
].filter(Boolean);
|
|
|
|
return candidates
|
|
.map(toRepoPath)
|
|
.find((item) => existsSync(join(repoRoot, item)));
|
|
}
|
|
|
|
function visit(node: ts.Node) {
|
|
if (
|
|
ts.isImportDeclaration(node) &&
|
|
ts.isStringLiteral(node.moduleSpecifier)
|
|
) {
|
|
const resolvedFile = resolveImportPath(node.moduleSpecifier.text);
|
|
const namedBindings = node.importClause?.namedBindings;
|
|
if (resolvedFile && namedBindings && ts.isNamedImports(namedBindings)) {
|
|
for (const element of namedBindings.elements) {
|
|
const imported = element.propertyName?.text ?? element.name.text;
|
|
const local = element.name.text;
|
|
importBindings.set(local, { file: resolvedFile, imported });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
const initializer = unwrapExpression(node.initializer);
|
|
if (initializer) {
|
|
const literal = literalText(initializer);
|
|
if (literal !== undefined) {
|
|
stringBindings.set(node.name.text, literal);
|
|
} else if (ts.isObjectLiteralExpression(initializer)) {
|
|
objectBindings.set(node.name.text, initializer);
|
|
} else {
|
|
const array = readArrayFromExpression(initializer, sf, {
|
|
stringBindings,
|
|
arrayBindings,
|
|
objectBindings,
|
|
specBindings,
|
|
importBindings,
|
|
});
|
|
if (array) arrayBindings.set(node.name.text, array);
|
|
}
|
|
|
|
if (
|
|
ts.isCallExpression(initializer) &&
|
|
ts.isIdentifier(initializer.expression) &&
|
|
initializer.expression.text === "requireActionSpec"
|
|
) {
|
|
const [firstArg] = initializer.arguments;
|
|
const specName =
|
|
literalText(firstArg) ||
|
|
(ts.isIdentifier(firstArg)
|
|
? stringBindings.get(firstArg.text)
|
|
: undefined);
|
|
if (specName) specBindings.set(node.name.text, specName);
|
|
}
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sf);
|
|
return {
|
|
stringBindings,
|
|
arrayBindings,
|
|
objectBindings,
|
|
specBindings,
|
|
importBindings,
|
|
};
|
|
}
|
|
|
|
function resolvedLiteralText(
|
|
repoRoot: string,
|
|
expr: ts.Expression | undefined,
|
|
bindings: StaticBindings,
|
|
): string | undefined {
|
|
const direct = literalText(expr);
|
|
if (direct !== undefined) return direct;
|
|
if (!expr) return undefined;
|
|
|
|
if (ts.isIdentifier(expr)) {
|
|
const imported = importedBindingValue(repoRoot, bindings, expr.text);
|
|
return (
|
|
bindings.stringBindings.get(expr.text) ||
|
|
(imported?.type === "string" ? imported.value : undefined)
|
|
);
|
|
}
|
|
|
|
if (
|
|
ts.isPropertyAccessExpression(expr) &&
|
|
expr.name.text === "name" &&
|
|
ts.isIdentifier(expr.expression)
|
|
) {
|
|
return bindings.specBindings.get(expr.expression.text);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function stringArray(
|
|
repoRoot: string,
|
|
expr: ts.Expression | undefined,
|
|
sf: ts.SourceFile,
|
|
bindings: StaticBindings,
|
|
options: { refs?: boolean } = {},
|
|
): string[] {
|
|
const unwrapped = unwrapExpression(expr);
|
|
if (!unwrapped) return [];
|
|
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
const arrayValue = bindings.arrayBindings.get(unwrapped.text);
|
|
if (arrayValue) return arrayValue;
|
|
const stringValue = bindings.stringBindings.get(unwrapped.text);
|
|
if (stringValue) return [stringValue];
|
|
const imported = importedBindingValue(repoRoot, bindings, unwrapped.text);
|
|
if (imported?.type === "array") return imported.value;
|
|
if (imported?.type === "string") return [imported.value];
|
|
return options.refs
|
|
? [`{ref:${unwrapped.text}}`]
|
|
: [exprText(unwrapped, sf, 120)];
|
|
}
|
|
|
|
if (!ts.isArrayLiteralExpression(unwrapped)) {
|
|
return [exprText(unwrapped, sf, 120)].filter(Boolean);
|
|
}
|
|
|
|
return unwrapped.elements.flatMap((element): string[] => {
|
|
if (ts.isStringLiteralLike(element)) return [element.text];
|
|
|
|
if (ts.isSpreadElement(element)) {
|
|
if (ts.isIdentifier(element.expression)) {
|
|
const imported = importedBindingValue(
|
|
repoRoot,
|
|
bindings,
|
|
element.expression.text,
|
|
);
|
|
if (imported?.type === "array") return imported.value;
|
|
const localArray = bindings.arrayBindings.get(element.expression.text);
|
|
if (localArray) return localArray;
|
|
}
|
|
return [`...${exprText(element.expression, sf, 80)}`];
|
|
}
|
|
|
|
if (ts.isIdentifier(element)) {
|
|
const stringValue = bindings.stringBindings.get(element.text);
|
|
if (stringValue) return [stringValue];
|
|
const arrayValue = bindings.arrayBindings.get(element.text);
|
|
if (arrayValue) return arrayValue;
|
|
const imported = importedBindingValue(repoRoot, bindings, element.text);
|
|
if (imported?.type === "array") return imported.value;
|
|
if (imported?.type === "string") return [imported.value];
|
|
return options.refs
|
|
? [`{ref:${element.text}}`]
|
|
: [exprText(element, sf, 100)];
|
|
}
|
|
|
|
if (ts.isObjectLiteralExpression(element)) {
|
|
const name = literalText(getPropExpression(getProp(element, "name")));
|
|
return name ? [`{inline:${name}}`] : [exprText(element, sf, 100)];
|
|
}
|
|
|
|
return [exprText(element, sf, 100)].filter(Boolean);
|
|
});
|
|
}
|
|
|
|
function declarationName(node: ts.ObjectLiteralExpression): string {
|
|
let current: ts.Node | undefined = node.parent;
|
|
while (current) {
|
|
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
|
|
return current.name.text;
|
|
}
|
|
if (ts.isPropertyAssignment(current)) return propName(current) ?? "";
|
|
if (ts.isExportAssignment(current)) return "default";
|
|
current = current.parent;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function nearestFunctionReturnType(node: ts.Node): string {
|
|
let current: ts.Node | undefined = node.parent;
|
|
while (current) {
|
|
if (
|
|
(ts.isFunctionDeclaration(current) ||
|
|
ts.isFunctionExpression(current) ||
|
|
ts.isArrowFunction(current) ||
|
|
ts.isMethodDeclaration(current)) &&
|
|
current.type
|
|
) {
|
|
return current.type.getText();
|
|
}
|
|
current = current.parent;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function declarationType(node: ts.ObjectLiteralExpression): string {
|
|
let current: ts.Node | undefined = node;
|
|
while (current?.parent) {
|
|
const parent: ts.Node = current.parent;
|
|
if (
|
|
ts.isVariableDeclaration(parent) &&
|
|
parent.initializer === current &&
|
|
parent.type
|
|
) {
|
|
return parent.type.getText();
|
|
}
|
|
if (
|
|
(ts.isAsExpression(parent) || ts.isSatisfiesExpression(parent)) &&
|
|
parent.expression === current
|
|
) {
|
|
return parent.type.getText();
|
|
}
|
|
if (ts.isReturnStatement(parent)) {
|
|
return nearestFunctionReturnType(parent);
|
|
}
|
|
current = parent;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function hasActionType(typeText: string): boolean {
|
|
return /\b\w*Action\b/.test(typeText);
|
|
}
|
|
|
|
function hasFullActionShape(obj: ts.ObjectLiteralExpression): boolean {
|
|
return Boolean(
|
|
getProp(obj, "name") &&
|
|
getProp(obj, "description") &&
|
|
getProp(obj, "parameters") &&
|
|
getProp(obj, "validate") &&
|
|
getProp(obj, "handler"),
|
|
);
|
|
}
|
|
|
|
function lineFor(sf: ts.SourceFile, node: ts.Node): number {
|
|
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
}
|
|
|
|
function sourceKind(file: string): string {
|
|
if (file.startsWith("packages/core/")) return "core";
|
|
if (file.startsWith("packages/agent/")) return "agent";
|
|
if (file.startsWith("packages/app-core/")) return "app-core";
|
|
const plugin = file.match(/^plugins\/([^/]+)\//);
|
|
if (plugin) return `plugin:${plugin[1]}`;
|
|
const cloudPlugin = file.match(
|
|
/^cloud\/packages\/lib\/eliza\/(plugin-[^/]+)\//,
|
|
);
|
|
if (cloudPlugin) return `cloud:${cloudPlugin[1]}`;
|
|
const cloudEliza = file.match(/^cloud\/packages\/lib\/eliza\/([^/]+)\//);
|
|
if (cloudEliza) return `cloud:${cloudEliza[1]}`;
|
|
if (file.startsWith("cloud/")) return "cloud";
|
|
if (file.startsWith("packages/")) return `package:${file.split("/")[1]}`;
|
|
return "repo";
|
|
}
|
|
|
|
function validationKind(
|
|
obj: ts.ObjectLiteralExpression,
|
|
sf: ts.SourceFile,
|
|
): ScannedAction["validation"] {
|
|
const validate = getProp(obj, "validate");
|
|
if (!validate) return "missing";
|
|
const text = validate.getText(sf);
|
|
return /=>\s*true|return\s+true\b/.test(text) ? "always_true" : "conditional";
|
|
}
|
|
|
|
function enumValuesFromSchema(
|
|
repoRoot: string,
|
|
schema: ts.Expression | undefined,
|
|
sf: ts.SourceFile,
|
|
bindings: StaticBindings,
|
|
): string[] {
|
|
const unwrapped = unwrapExpression(schema);
|
|
if (!unwrapped || !ts.isObjectLiteralExpression(unwrapped)) return [];
|
|
const enumExpr =
|
|
getPropExpression(getProp(unwrapped, "enum")) ||
|
|
getPropExpression(getProp(unwrapped, "enumValues"));
|
|
return stringArray(repoRoot, enumExpr, sf, bindings);
|
|
}
|
|
|
|
function schemaType(
|
|
schema: ts.Expression | undefined,
|
|
sf: ts.SourceFile,
|
|
): string | undefined {
|
|
const unwrapped = unwrapExpression(schema);
|
|
if (!unwrapped || !ts.isObjectLiteralExpression(unwrapped)) return undefined;
|
|
const typeExpr = getPropExpression(getProp(unwrapped, "type"));
|
|
return literalText(typeExpr) ?? (exprText(typeExpr, sf, 80) || undefined);
|
|
}
|
|
|
|
function parameterFromObject(
|
|
repoRoot: string,
|
|
obj: ts.ObjectLiteralExpression,
|
|
sf: ts.SourceFile,
|
|
bindings: StaticBindings,
|
|
): ScannedParameter {
|
|
const nameExpr = getPropExpression(getProp(obj, "name"));
|
|
const descriptionExpr = getPropExpression(
|
|
getProp(obj, [
|
|
"descriptionCompressed",
|
|
"compressedDescription",
|
|
"description",
|
|
]),
|
|
);
|
|
const requiredExpr = getPropExpression(getProp(obj, "required"));
|
|
const schemaExpr = getPropExpression(getProp(obj, "schema"));
|
|
const name =
|
|
resolvedLiteralText(repoRoot, nameExpr, bindings) ??
|
|
exprText(nameExpr, sf, 100);
|
|
const description =
|
|
resolvedLiteralText(repoRoot, descriptionExpr, bindings) ??
|
|
exprText(descriptionExpr, sf, 180);
|
|
|
|
return {
|
|
name,
|
|
description,
|
|
required: literalText(requiredExpr) === "true" ? true : undefined,
|
|
schemaType: schemaType(schemaExpr, sf),
|
|
enumValues: enumValuesFromSchema(repoRoot, schemaExpr, sf, bindings),
|
|
source: exprText(obj, sf, 220),
|
|
};
|
|
}
|
|
|
|
function parameterDetails(
|
|
repoRoot: string,
|
|
expr: ts.Expression | undefined,
|
|
sf: ts.SourceFile,
|
|
bindings: StaticBindings,
|
|
): ScannedParameter[] {
|
|
const unwrapped = unwrapExpression(expr);
|
|
if (!unwrapped) return [];
|
|
if (!ts.isArrayLiteralExpression(unwrapped)) {
|
|
return [
|
|
{
|
|
name: exprText(unwrapped, sf, 100),
|
|
description: "",
|
|
enumValues: [],
|
|
source: exprText(unwrapped, sf, 180),
|
|
},
|
|
].filter((p) => p.name);
|
|
}
|
|
|
|
return unwrapped.elements.flatMap((element): ScannedParameter[] => {
|
|
if (ts.isSpreadElement(element)) {
|
|
return [
|
|
{
|
|
name: `...${exprText(element.expression, sf, 80)}`,
|
|
description: "",
|
|
enumValues: [],
|
|
source: exprText(element, sf, 180),
|
|
},
|
|
];
|
|
}
|
|
|
|
if (ts.isIdentifier(element)) {
|
|
const object = bindings.objectBindings.get(element.text);
|
|
if (object) return [parameterFromObject(repoRoot, object, sf, bindings)];
|
|
return [
|
|
{
|
|
name: `{ref:${element.text}}`,
|
|
description: "",
|
|
enumValues: [],
|
|
source: exprText(element, sf, 120),
|
|
},
|
|
];
|
|
}
|
|
|
|
if (!ts.isObjectLiteralExpression(element)) {
|
|
return [
|
|
{
|
|
name: exprText(element, sf, 100),
|
|
description: "",
|
|
enumValues: [],
|
|
source: exprText(element, sf, 180),
|
|
},
|
|
].filter((p) => p.name);
|
|
}
|
|
|
|
return [parameterFromObject(repoRoot, element, sf, bindings)];
|
|
});
|
|
}
|
|
|
|
function parameterSummary(parameters: ScannedParameter[]): string[] {
|
|
return parameters.map((param) => {
|
|
const required = param.required ? " required" : "";
|
|
const schema = param.schemaType ? `:${param.schemaType}` : "";
|
|
const enumText = param.enumValues.length
|
|
? ` [${param.enumValues.join("|")}]`
|
|
: "";
|
|
return `${param.name}${schema}${required}${enumText}`;
|
|
});
|
|
}
|
|
|
|
function subActionsFromDescription(description: string): string[] {
|
|
const match = description.match(
|
|
/(?:one of|subactions?|operations?|ops?|op)\s*(?:to perform|to run|are|is)?\s*:\s*([^.\n]+)/i,
|
|
);
|
|
if (!match) return [];
|
|
return match[1]
|
|
.replace(/\bor\b/gi, ",")
|
|
.split(/[,|]/)
|
|
.map((value) => value.trim().replace(/[`"'()]/g, ""))
|
|
.map((value) => value.split(/\s+/)[0] ?? "")
|
|
.filter((value) => /^[a-z0-9_-]{2,64}$/i.test(value));
|
|
}
|
|
|
|
function inferredSubActions(
|
|
parameters: ScannedParameter[],
|
|
): InferredSubAction[] {
|
|
const byName = new Map<string, InferredSubAction>();
|
|
for (const param of parameters) {
|
|
if (!/^(subaction|subactions|op|operation|mode)$/i.test(param.name))
|
|
continue;
|
|
|
|
for (const value of param.enumValues) {
|
|
byName.set(value, {
|
|
name: value,
|
|
parameter: param.name,
|
|
source: "parameter-enum",
|
|
});
|
|
}
|
|
|
|
if (param.enumValues.length === 0) {
|
|
for (const value of subActionsFromDescription(param.description)) {
|
|
byName.set(value, {
|
|
name: value,
|
|
parameter: param.name,
|
|
source: "parameter-description",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return [...byName.values()];
|
|
}
|
|
|
|
function parseSubActionRaw(
|
|
raw: string,
|
|
): Pick<SubActionReference, "kind" | "name"> {
|
|
const ref = raw.match(/^\{ref:(.+)\}$/)?.[1];
|
|
if (ref) return { kind: "ref", name: ref };
|
|
|
|
const inline = raw.match(/^\{inline:(.+)\}$/)?.[1];
|
|
if (inline) return { kind: "inline", name: inline };
|
|
|
|
if (raw.startsWith("...")) return { kind: "spread", name: raw.slice(3) };
|
|
if (raw.includes("(") || raw.includes("{") || raw.includes("=>")) {
|
|
return { kind: "expression", name: raw };
|
|
}
|
|
return { kind: "name", name: raw };
|
|
}
|
|
|
|
function makeActionRecord(
|
|
repoRoot: string,
|
|
file: string,
|
|
sf: ts.SourceFile,
|
|
obj: ts.ObjectLiteralExpression,
|
|
bindings: StaticBindings,
|
|
detectedBy: ScannedAction["detectedBy"],
|
|
actionType: string,
|
|
): Omit<ScannedAction, "resolvedSubActions" | "parentIds" | "parentNames"> {
|
|
const nameExpr = getPropExpression(getProp(obj, "name"));
|
|
const resolvedName = resolvedLiteralText(repoRoot, nameExpr, bindings);
|
|
const name = resolvedName ?? exprText(nameExpr, sf, 120);
|
|
const descriptionExpr = getPropExpression(
|
|
getProp(obj, [
|
|
"descriptionCompressed",
|
|
"compressedDescription",
|
|
"description",
|
|
]),
|
|
);
|
|
const description =
|
|
resolvedLiteralText(repoRoot, descriptionExpr, bindings) ??
|
|
exprText(descriptionExpr, sf, 500);
|
|
const parameters = parameterDetails(
|
|
repoRoot,
|
|
getPropExpression(getProp(obj, "parameters")),
|
|
sf,
|
|
bindings,
|
|
);
|
|
const subActions = stringArray(
|
|
repoRoot,
|
|
getPropExpression(getProp(obj, "subActions")),
|
|
sf,
|
|
bindings,
|
|
{ refs: true },
|
|
);
|
|
const line = lineFor(sf, obj);
|
|
|
|
return {
|
|
id: `${file}:${line}:${declarationName(obj) || name || obj.pos}`,
|
|
name,
|
|
nameStatic: Boolean(resolvedName),
|
|
declarationName: declarationName(obj),
|
|
actionType,
|
|
detectedBy,
|
|
source: sourceKind(file),
|
|
file,
|
|
absoluteFile: join(repoRoot, file),
|
|
line,
|
|
description,
|
|
contexts: stringArray(
|
|
repoRoot,
|
|
getPropExpression(getProp(obj, "contexts")),
|
|
sf,
|
|
bindings,
|
|
),
|
|
tags: stringArray(
|
|
repoRoot,
|
|
getPropExpression(getProp(obj, "tags")),
|
|
sf,
|
|
bindings,
|
|
),
|
|
similes: stringArray(
|
|
repoRoot,
|
|
getPropExpression(getProp(obj, "similes")),
|
|
sf,
|
|
bindings,
|
|
),
|
|
mode: propSource(obj, "mode", sf, 100),
|
|
roleGate: propSource(obj, "roleGate", sf, 180),
|
|
contextGate: propSource(obj, "contextGate", sf, 220),
|
|
validation: validationKind(obj, sf),
|
|
subPlanner: Boolean(getProp(obj, "subPlanner")),
|
|
parameters,
|
|
parameterSummary: parameterSummary(parameters),
|
|
subActions,
|
|
inferredSubActions: inferredSubActions(parameters),
|
|
};
|
|
}
|
|
|
|
function scanFile(
|
|
repoRoot: string,
|
|
file: string,
|
|
): Array<
|
|
Omit<ScannedAction, "resolvedSubActions" | "parentIds" | "parentNames">
|
|
> {
|
|
const abs = join(repoRoot, file);
|
|
const source = readFileSync(abs, "utf8");
|
|
const sf = ts.createSourceFile(
|
|
file,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
);
|
|
const bindings = collectStaticBindings(repoRoot, sf, file);
|
|
const records: Array<
|
|
Omit<ScannedAction, "resolvedSubActions" | "parentIds" | "parentNames">
|
|
> = [];
|
|
const seen = new Set<number>();
|
|
|
|
function visit(node: ts.Node) {
|
|
if (ts.isObjectLiteralExpression(node) && !seen.has(node.pos)) {
|
|
seen.add(node.pos);
|
|
const type = declarationType(node);
|
|
const detectedBy: ScannedAction["detectedBy"] | null = hasFullActionShape(
|
|
node,
|
|
)
|
|
? "shape"
|
|
: hasActionType(type) &&
|
|
getProp(node, "name") &&
|
|
getProp(node, "validate") &&
|
|
getProp(node, "handler")
|
|
? "type"
|
|
: null;
|
|
|
|
if (detectedBy) {
|
|
records.push(
|
|
makeActionRecord(
|
|
repoRoot,
|
|
file,
|
|
sf,
|
|
node,
|
|
bindings,
|
|
detectedBy,
|
|
type,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sf);
|
|
return records;
|
|
}
|
|
|
|
function fallbackWalk(repoRoot: string): string[] {
|
|
const files: string[] = [];
|
|
const roots = SOURCE_ROOTS.map((root) => join(repoRoot, root)).filter(
|
|
existsSync,
|
|
);
|
|
|
|
function walk(dir: string) {
|
|
for (const entry of readdirSync(dir)) {
|
|
const abs = join(dir, entry);
|
|
const rel = toRepoPath(relative(repoRoot, abs));
|
|
if (EXCLUDE_RE.test(rel)) continue;
|
|
const stat = statSync(abs);
|
|
if (stat.isDirectory()) {
|
|
walk(abs);
|
|
} else if (/\.(ts|tsx)$/.test(entry)) {
|
|
files.push(rel);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const root of roots) walk(root);
|
|
return files;
|
|
}
|
|
|
|
function gitFiles(repoRoot: string): string[] {
|
|
try {
|
|
const output = execFileSync(
|
|
"git",
|
|
["ls-files", "-co", "--exclude-standard", ...SOURCE_ROOTS],
|
|
{
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
},
|
|
);
|
|
return output
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map(toRepoPath)
|
|
.filter((file) => existsSync(join(repoRoot, file)))
|
|
.filter((file) => /\.(ts|tsx)$/.test(file))
|
|
.filter((file) => !EXCLUDE_RE.test(file));
|
|
} catch {
|
|
return fallbackWalk(repoRoot);
|
|
}
|
|
}
|
|
|
|
function sortActions(
|
|
actions: ScannedAction[],
|
|
sort: ActionScanSort,
|
|
): ScannedAction[] {
|
|
const sorted = [...actions];
|
|
sorted.sort((a, b) => {
|
|
if (sort === "filepath") {
|
|
return (
|
|
a.file.localeCompare(b.file) ||
|
|
a.line - b.line ||
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
}
|
|
return (
|
|
a.name.localeCompare(b.name) ||
|
|
a.file.localeCompare(b.file) ||
|
|
a.line - b.line
|
|
);
|
|
});
|
|
return sorted;
|
|
}
|
|
|
|
function resolveSubActionRefs(actions: ScannedAction[]): void {
|
|
const byName = new Map<string, ScannedAction>();
|
|
const byDecl = new Map<string, ScannedAction>();
|
|
const byFileAndDecl = new Map<string, ScannedAction>();
|
|
|
|
for (const action of actions) {
|
|
if (action.name && !byName.has(action.name))
|
|
byName.set(action.name, action);
|
|
if (action.declarationName && !byDecl.has(action.declarationName)) {
|
|
byDecl.set(action.declarationName, action);
|
|
}
|
|
if (action.declarationName) {
|
|
byFileAndDecl.set(`${action.file}:${action.declarationName}`, action);
|
|
}
|
|
}
|
|
|
|
for (const action of actions) {
|
|
action.resolvedSubActions = action.subActions.map((raw) => {
|
|
const parsed = parseSubActionRaw(raw);
|
|
const imported =
|
|
parsed.kind === "ref"
|
|
? actionImports(action.file, parsed.name)
|
|
: undefined;
|
|
const importedTarget = imported
|
|
? byFileAndDecl.get(`${imported.file}:${imported.imported}`)
|
|
: undefined;
|
|
const target =
|
|
importedTarget ??
|
|
(parsed.kind === "ref"
|
|
? byDecl.get(parsed.name)
|
|
: byName.get(parsed.name));
|
|
|
|
return {
|
|
raw,
|
|
name: target?.name ?? parsed.name,
|
|
kind: parsed.kind,
|
|
found: Boolean(target),
|
|
targetId: target?.id,
|
|
targetFile: target?.file,
|
|
targetLine: target?.line,
|
|
};
|
|
});
|
|
}
|
|
|
|
const actionById = new Map(actions.map((action) => [action.id, action]));
|
|
for (const action of actions) {
|
|
for (const child of action.resolvedSubActions) {
|
|
if (!child.targetId) continue;
|
|
const target = actionById.get(child.targetId);
|
|
if (!target) continue;
|
|
target.parentIds.push(action.id);
|
|
target.parentNames.push(action.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
function actionImports(
|
|
file: string,
|
|
localName: string,
|
|
): ImportedBinding | undefined {
|
|
const importer = importBindingIndex.get(file);
|
|
if (importer) return importer.get(localName);
|
|
|
|
return undefined;
|
|
}
|
|
|
|
const importBindingIndex = new Map<string, Map<string, ImportedBinding>>();
|
|
|
|
function collectImportBindingIndex(repoRoot: string, files: string[]): void {
|
|
importBindingIndex.clear();
|
|
for (const file of files) {
|
|
try {
|
|
const source = readFileSync(join(repoRoot, file), "utf8");
|
|
const sf = ts.createSourceFile(
|
|
file,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
);
|
|
importBindingIndex.set(
|
|
file,
|
|
collectStaticBindings(repoRoot, sf, file).importBindings,
|
|
);
|
|
} catch {
|
|
importBindingIndex.set(file, new Map());
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeTreeNode(
|
|
action: ScannedAction,
|
|
actionById: Map<string, ScannedAction>,
|
|
sort: ActionScanSort,
|
|
seen = new Set<string>(),
|
|
): ActionTreeNode {
|
|
const cycle = seen.has(action.id);
|
|
const node: ActionTreeNode = {
|
|
id: action.id,
|
|
name: action.name,
|
|
kind: "action",
|
|
file: action.file,
|
|
absoluteFile: action.absoluteFile,
|
|
line: action.line,
|
|
source: action.source,
|
|
actionId: action.id,
|
|
cycle,
|
|
children: [],
|
|
};
|
|
if (cycle) return node;
|
|
|
|
const nextSeen = new Set(seen);
|
|
nextSeen.add(action.id);
|
|
|
|
const actionChildren = action.resolvedSubActions.map(
|
|
(child): ActionTreeNode => {
|
|
const target = child.targetId
|
|
? actionById.get(child.targetId)
|
|
: undefined;
|
|
if (target) return makeTreeNode(target, actionById, sort, nextSeen);
|
|
return {
|
|
id: `${action.id}:missing:${child.raw}`,
|
|
name: child.name,
|
|
kind: "missing",
|
|
ref: child.raw,
|
|
children: [],
|
|
};
|
|
},
|
|
);
|
|
|
|
const inferredChildren = action.inferredSubActions.map(
|
|
(child): ActionTreeNode => ({
|
|
id: `${action.id}:inferred:${child.parameter}:${child.name}`,
|
|
name: child.name,
|
|
kind: "inferred",
|
|
parameter: child.parameter,
|
|
inferredSource: child.source,
|
|
children: [],
|
|
}),
|
|
);
|
|
|
|
node.children = sortTreeNodes([...actionChildren, ...inferredChildren], sort);
|
|
return node;
|
|
}
|
|
|
|
function sortTreeNodes(
|
|
nodes: ActionTreeNode[],
|
|
sort: ActionScanSort,
|
|
): ActionTreeNode[] {
|
|
return [...nodes].sort((a, b) => {
|
|
if (sort === "filepath") {
|
|
return (
|
|
(a.file ?? "").localeCompare(b.file ?? "") ||
|
|
(a.line ?? 0) - (b.line ?? 0) ||
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
}
|
|
return (
|
|
a.name.localeCompare(b.name) ||
|
|
(a.file ?? "").localeCompare(b.file ?? "") ||
|
|
(a.line ?? 0) - (b.line ?? 0)
|
|
);
|
|
});
|
|
}
|
|
|
|
function sourceGroups(
|
|
actions: ScannedAction[],
|
|
): Array<{ source: string; count: number }> {
|
|
const counts = new Map<string, number>();
|
|
for (const action of actions) {
|
|
counts.set(action.source, (counts.get(action.source) ?? 0) + 1);
|
|
}
|
|
return [...counts.entries()]
|
|
.map(([source, count]) => ({ source, count }))
|
|
.sort((a, b) => b.count - a.count || a.source.localeCompare(b.source));
|
|
}
|
|
|
|
export function scanRepoActions(options: {
|
|
repoRoot: string;
|
|
sort?: ActionScanSort;
|
|
}): ActionScanResult {
|
|
const repoRoot = resolve(options.repoRoot);
|
|
const sort = options.sort ?? "name";
|
|
exportedBindingCache.clear();
|
|
|
|
const files = gitFiles(repoRoot);
|
|
collectImportBindingIndex(repoRoot, files);
|
|
|
|
const actions: ScannedAction[] = files
|
|
.flatMap((file) => scanFile(repoRoot, file))
|
|
.map((action) => ({
|
|
...action,
|
|
resolvedSubActions: [],
|
|
parentIds: [],
|
|
parentNames: [],
|
|
}));
|
|
|
|
resolveSubActionRefs(actions);
|
|
|
|
const actionById = new Map(actions.map((action) => [action.id, action]));
|
|
const sortedActions = sortActions(actions, sort);
|
|
const childIds = new Set(
|
|
actions.flatMap((action) =>
|
|
action.resolvedSubActions.flatMap((child) =>
|
|
child.targetId ? [child.targetId] : [],
|
|
),
|
|
),
|
|
);
|
|
const roots = sortActions(
|
|
actions.filter((action) => !childIds.has(action.id)),
|
|
sort,
|
|
).map((action) => makeTreeNode(action, actionById, sort));
|
|
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
repoRoot,
|
|
sourceRoots: SOURCE_ROOTS,
|
|
filesScanned: files.length,
|
|
actionCount: actions.length,
|
|
rootCount: roots.length,
|
|
parentCount: actions.filter(
|
|
(action) =>
|
|
action.resolvedSubActions.length > 0 ||
|
|
action.inferredSubActions.length > 0,
|
|
).length,
|
|
unresolvedSubActionCount: actions.reduce(
|
|
(count, action) =>
|
|
count +
|
|
action.resolvedSubActions.filter((child) => !child.found).length,
|
|
0,
|
|
),
|
|
inferredSubActionCount: actions.reduce(
|
|
(count, action) => count + action.inferredSubActions.length,
|
|
0,
|
|
),
|
|
dynamicNameCount: actions.filter((action) => !action.nameStatic).length,
|
|
sort,
|
|
sourceGroups: sourceGroups(actions),
|
|
roots,
|
|
actions: sortedActions,
|
|
};
|
|
}
|