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
520 lines
15 KiB
JavaScript
520 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Static audit for action availability gates.
|
|
*
|
|
* This is intentionally heuristic: it finds action-like object literals, records
|
|
* role/context/validate metadata, and flags validate() bodies that still look
|
|
* intent/keyword based instead of hard state based.
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { join, relative } from "node:path";
|
|
import ts from "typescript";
|
|
|
|
const ROOT = process.cwd();
|
|
const DEFAULT_ROOTS = [
|
|
"packages/core/src",
|
|
"packages/agent/src/actions",
|
|
"plugins",
|
|
"packages/cloud/shared/src/lib/eliza",
|
|
];
|
|
const SKIP_DIRS = new Set([
|
|
"node_modules",
|
|
"dist",
|
|
"build",
|
|
"coverage",
|
|
".turbo",
|
|
"__tests__",
|
|
"generated",
|
|
"test",
|
|
"tests",
|
|
]);
|
|
|
|
const KEYWORD_FILES = [
|
|
"packages/shared/src/i18n/keywords/action-search.generated.keywords.json",
|
|
"packages/shared/src/i18n/keywords/context-search.keywords.json",
|
|
"packages/shared/src/i18n/keywords/shared.keywords.json",
|
|
"packages/shared/src/i18n/keywords/typescript.keywords.json",
|
|
"packages/shared/src/i18n/keywords/validate.keywords.json",
|
|
];
|
|
|
|
const CONTEXT_CONSTANTS = {
|
|
CODING_TOOLS_CONTEXTS: ["code", "terminal", "automation"],
|
|
POST_CONTEXTS: ["social_posting", "connectors"],
|
|
ROOM_CONTEXTS: ["messaging", "contacts", "settings"],
|
|
TODOS_CONTEXTS: ["tasks", "todos", "automation"],
|
|
};
|
|
|
|
const args = new Set(process.argv.slice(2));
|
|
const format = args.has("--json") ? "json" : "markdown";
|
|
const roots = process.argv
|
|
.slice(2)
|
|
.filter((arg) => !arg.startsWith("--"))
|
|
.filter(Boolean);
|
|
|
|
const keywordKeys = loadKeywordKeys();
|
|
const files = (roots.length > 0 ? roots : DEFAULT_ROOTS).flatMap((root) =>
|
|
walk(join(ROOT, root)),
|
|
);
|
|
const actions = files
|
|
.flatMap(scanFile)
|
|
.sort((a, b) => a.name.localeCompare(b.name) || a.file.localeCompare(b.file));
|
|
const summary = buildSummary(actions);
|
|
|
|
if (format === "json") {
|
|
console.log(JSON.stringify({ summary, actions }, null, 2));
|
|
} else {
|
|
console.log(renderMarkdown(summary, actions));
|
|
}
|
|
|
|
function loadKeywordKeys() {
|
|
const keys = new Set();
|
|
for (const file of KEYWORD_FILES) {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(join(ROOT, file), "utf8"));
|
|
for (const key of Object.keys(parsed.entries ?? {})) {
|
|
keys.add(key);
|
|
}
|
|
} catch {
|
|
// Missing keyword files should not prevent source auditing.
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
function walk(dir) {
|
|
let out = [];
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(dir);
|
|
} catch {
|
|
return out;
|
|
}
|
|
for (const entry of entries) {
|
|
const full = join(dir, entry);
|
|
let stat;
|
|
try {
|
|
stat = statSync(full);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (stat.isDirectory()) {
|
|
if (!SKIP_DIRS.has(entry)) out = out.concat(walk(full));
|
|
continue;
|
|
}
|
|
if (!/\.(ts|tsx|js|mjs)$/.test(entry)) continue;
|
|
if (/\.(test|spec)\.(ts|tsx|js|mjs)$/.test(entry)) continue;
|
|
out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function scanFile(file) {
|
|
const rel = relative(ROOT, file);
|
|
if (
|
|
/(^|\/)routes?\//.test(rel) ||
|
|
/(^|\/)(routes|setup-routes)\.(ts|tsx|js|mjs)$/.test(rel)
|
|
) {
|
|
return [];
|
|
}
|
|
const text = readFileSync(file, "utf8");
|
|
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
const found = [];
|
|
|
|
function visit(node) {
|
|
if (ts.isObjectLiteralExpression(node)) {
|
|
const action = readActionObject(source, node);
|
|
if (action) found.push(action);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(source);
|
|
return found;
|
|
}
|
|
|
|
function readActionObject(source, node) {
|
|
const name = getStringProperty(node, "name");
|
|
if (!name) return null;
|
|
if (isEvaluatorObject(source, node)) return null;
|
|
|
|
const hasHandler = Boolean(getProperty(node, "handler"));
|
|
const hasValidate = Boolean(getProperty(node, "validate"));
|
|
const hasActionMetadata = [
|
|
"contexts",
|
|
"contextGate",
|
|
"description",
|
|
"descriptionCompressed",
|
|
"examples",
|
|
"mode",
|
|
"parameters",
|
|
"roleGate",
|
|
"similes",
|
|
"subActions",
|
|
].some((propertyName) => Boolean(getProperty(node, propertyName)));
|
|
const hasActionShape = (hasHandler || hasValidate) && hasActionMetadata;
|
|
if (!hasActionShape) return null;
|
|
|
|
const validateNode = getPropertyInitializer(node, "validate");
|
|
const validateText = validateNode?.getText(source) ?? "";
|
|
const modeText = getPropertyInitializer(node, "mode")?.getText(source) ?? "";
|
|
const contexts = getStringArrayProperty(node, "contexts");
|
|
const similes = getStringArrayProperty(node, "similes");
|
|
const description = getStringProperty(node, "description");
|
|
const descriptionCompressed =
|
|
getStringProperty(node, "descriptionCompressed") ??
|
|
getStringProperty(node, "compressedDescription");
|
|
const line =
|
|
source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
const keywordStem = actionNameToKeywordStem(name);
|
|
const matchedKeywordKeys = [...keywordKeys]
|
|
.filter((key) => key.startsWith(`action.${keywordStem}.`))
|
|
.sort();
|
|
|
|
const flags = classifyValidate(validateText, hasValidate);
|
|
|
|
return {
|
|
name,
|
|
file: relative(ROOT, source.fileName),
|
|
line,
|
|
contexts,
|
|
similes,
|
|
description,
|
|
descriptionCompressed,
|
|
hasRoleGate: Boolean(getProperty(node, "roleGate")),
|
|
hasContextGate: Boolean(getProperty(node, "contextGate")),
|
|
hasSubActions: Boolean(getProperty(node, "subActions")),
|
|
hasConnectorAccountPolicy:
|
|
Boolean(getProperty(node, "connectorAccountPolicy")) ||
|
|
Boolean(getProperty(node, "accountPolicy")),
|
|
isInternalMode: /ActionMode\.ALWAYS_(?:BEFORE|DURING|AFTER)/.test(modeText),
|
|
validate: flags,
|
|
keywordKeys: matchedKeywordKeys,
|
|
needsHardStateReview: needsHardStateReview({
|
|
name,
|
|
contexts,
|
|
flags,
|
|
hasConnectorAccountPolicy:
|
|
Boolean(getProperty(node, "connectorAccountPolicy")) ||
|
|
Boolean(getProperty(node, "accountPolicy")),
|
|
}),
|
|
};
|
|
}
|
|
|
|
function isEvaluatorObject(source, node) {
|
|
const parent = node.parent;
|
|
if (
|
|
ts.isVariableDeclaration(parent) &&
|
|
parent.type?.getText(source).includes("Evaluator")
|
|
) {
|
|
return true;
|
|
}
|
|
if (
|
|
ts.isPropertyAssignment(parent) &&
|
|
parent.name &&
|
|
/evaluator/i.test(parent.name.getText(source))
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function classifyValidate(text, hasValidate) {
|
|
if (!hasValidate) {
|
|
return {
|
|
kind: "missing",
|
|
intentKeywordSignals: [],
|
|
stateSignals: [],
|
|
notes: ["missing validate()"],
|
|
};
|
|
}
|
|
const compact = text.replace(/\s+/g, " ").trim();
|
|
const intentKeywordSignals = matchAll(compact, [
|
|
"validateActionKeywords",
|
|
"getValidationKeywordTerms",
|
|
"findKeywordTermMatch",
|
|
"collectKeywordTermMatches",
|
|
"hasSelectedContextOrSignal",
|
|
"hasMediaActionSignal",
|
|
"looksLike[A-Za-z0-9_]*Intent",
|
|
"KEYWORD_HEURISTIC",
|
|
"recentMessages",
|
|
"message\\.content\\.text",
|
|
"content\\.text",
|
|
"getMessageText\\(",
|
|
"textOf\\(",
|
|
"messageText\\(",
|
|
"selectRoute\\(",
|
|
"extractSkillSlug\\(",
|
|
]);
|
|
const stateSignals = matchAll(compact, [
|
|
"getService",
|
|
"get[A-Za-z0-9_]*Service",
|
|
"getSetting",
|
|
"hasConnectedCapability",
|
|
"hasShopifyConfig",
|
|
"runtime\\.getCache",
|
|
"runtime\\.getRoom",
|
|
"runtime\\.getService",
|
|
"process\\.env",
|
|
"createEvmActionValidator",
|
|
"create[A-Za-z0-9_]*ActionValidator",
|
|
"createVisionProvider",
|
|
"extractId",
|
|
"extractTitle",
|
|
"connectorAccountPolicy",
|
|
"accountPolicy",
|
|
"getParticipantUserState",
|
|
"getPluginManager",
|
|
"getRoom",
|
|
"getWorld",
|
|
"hasActionContextOrKeyword",
|
|
"hasSelectedContext",
|
|
"hasLifeOpsAccess",
|
|
"hasLinearAccess",
|
|
"checkSenderRole",
|
|
"hasRoleAccess",
|
|
"hasExplicitPayload",
|
|
"content\\.source",
|
|
"character\\?\\.settings",
|
|
"registeredSkillSlugs",
|
|
"triggersFeatureEnabled",
|
|
"selectedContextMatches",
|
|
"serviceFromRuntime",
|
|
"validate[A-Za-z0-9_]*Availability",
|
|
"validateLinearActionIntent",
|
|
"validateMessageAction",
|
|
"validateRouter",
|
|
"listConversationAttachments",
|
|
"runtime\\.agentId",
|
|
"message\\.entityId",
|
|
"state\\.data",
|
|
"pageScope",
|
|
"feature",
|
|
"fetch",
|
|
"configured",
|
|
"connected",
|
|
"available",
|
|
"enabled",
|
|
"muted",
|
|
"FOLLOWED",
|
|
"MUTED",
|
|
]);
|
|
const alwaysTrue =
|
|
/^(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=]+)?=>\s*true$/.test(
|
|
compact,
|
|
) || /return true;?\s*}?$/.test(compact);
|
|
|
|
let kind = "unclear";
|
|
const notes = [];
|
|
if (intentKeywordSignals.length > 0) {
|
|
kind = "keyword_or_intent";
|
|
notes.push("move keyword/intent matching to action retrieval");
|
|
} else if (/^validate(?:\s+as\s+.+)?$/.test(compact)) {
|
|
kind = "external_reference";
|
|
notes.push("validate() is referenced outside this object literal");
|
|
} else if (stateSignals.length > 0) {
|
|
kind = "state_based";
|
|
notes.push("has hard state/service/account signals");
|
|
} else if (alwaysTrue) {
|
|
kind = "always_available";
|
|
notes.push("always true after context/role gates");
|
|
}
|
|
|
|
return {
|
|
kind,
|
|
intentKeywordSignals,
|
|
stateSignals,
|
|
notes,
|
|
};
|
|
}
|
|
|
|
function needsHardStateReview({
|
|
name,
|
|
contexts,
|
|
flags,
|
|
hasConnectorAccountPolicy,
|
|
}) {
|
|
const sensitiveContext = contexts.some((context) =>
|
|
[
|
|
"admin",
|
|
"agent_internal",
|
|
"automation",
|
|
"browser",
|
|
"code",
|
|
"connectors",
|
|
"email",
|
|
"files",
|
|
"finance",
|
|
"health",
|
|
"messaging",
|
|
"payments",
|
|
"phone",
|
|
"screen_time",
|
|
"secrets",
|
|
"settings",
|
|
"social",
|
|
"social_posting",
|
|
"subscriptions",
|
|
"terminal",
|
|
"wallet",
|
|
].includes(context),
|
|
);
|
|
const statefulName =
|
|
/(MUTE|UNMUTE|FOLLOW|UNFOLLOW|SEND|DELETE|UPDATE|SET|CREATE|CONNECT|DISCONNECT|BASH|TERMINAL|WALLET|PAY|SECRET|ROLE|PLUGIN|WORKFLOW|TRIGGER)/.test(
|
|
name,
|
|
);
|
|
if (flags.kind === "keyword_or_intent" || flags.kind === "missing")
|
|
return true;
|
|
if (hasConnectorAccountPolicy) return false;
|
|
if (flags.kind === "always_available") return false;
|
|
return sensitiveContext && statefulName && flags.kind !== "state_based";
|
|
}
|
|
|
|
function buildSummary(items) {
|
|
return {
|
|
totalActions: items.length,
|
|
internalModeActions: items.filter((item) => item.isInternalMode).length,
|
|
missingRoleGate: items.filter(
|
|
(item) => !item.isInternalMode && !item.hasRoleGate,
|
|
).length,
|
|
missingValidate: items.filter((item) => item.validate.kind === "missing")
|
|
.length,
|
|
keywordOrIntentValidate: items.filter(
|
|
(item) => item.validate.kind === "keyword_or_intent",
|
|
).length,
|
|
unclearValidate: items.filter((item) => item.validate.kind === "unclear")
|
|
.length,
|
|
externalReferenceValidate: items.filter(
|
|
(item) => item.validate.kind === "external_reference",
|
|
).length,
|
|
stateBasedValidate: items.filter(
|
|
(item) => item.validate.kind === "state_based",
|
|
).length,
|
|
alwaysAvailableValidate: items.filter(
|
|
(item) => item.validate.kind === "always_available",
|
|
).length,
|
|
missingExternalActionKeywords: items.filter(
|
|
(item) => item.keywordKeys.length === 0,
|
|
).length,
|
|
needsHardStateReview: items.filter((item) => item.needsHardStateReview)
|
|
.length,
|
|
};
|
|
}
|
|
|
|
function renderMarkdown(summary, items) {
|
|
const lines = [
|
|
"# Action Availability Audit",
|
|
"",
|
|
`Generated: ${new Date().toISOString()}`,
|
|
"",
|
|
"## Summary",
|
|
"",
|
|
`- Total action-like objects: ${summary.totalActions}`,
|
|
`- Internal mode actions/evaluators: ${summary.internalModeActions}`,
|
|
`- Missing explicit roleGate: ${summary.missingRoleGate}`,
|
|
`- Missing validate(): ${summary.missingValidate}`,
|
|
`- Keyword/intent validate(): ${summary.keywordOrIntentValidate}`,
|
|
`- Unclear validate(): ${summary.unclearValidate}`,
|
|
`- External-reference validate(): ${summary.externalReferenceValidate}`,
|
|
`- State-based validate(): ${summary.stateBasedValidate}`,
|
|
`- Always-available validate(): ${summary.alwaysAvailableValidate}`,
|
|
`- Missing external action keyword keys: ${summary.missingExternalActionKeywords}`,
|
|
`- Needs hard-state review: ${summary.needsHardStateReview}`,
|
|
"",
|
|
"## Actions",
|
|
"",
|
|
"| Action | File | Contexts | Role | Validate | Keywords | Review |",
|
|
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
];
|
|
|
|
for (const item of items) {
|
|
lines.push(
|
|
`${[
|
|
escapeCell(item.name),
|
|
escapeCell(`${item.file}:${item.line}`),
|
|
escapeCell(item.contexts.join(", ") || "-"),
|
|
item.hasRoleGate ? "yes" : item.isInternalMode ? "internal" : "missing",
|
|
escapeCell(item.validate.kind),
|
|
escapeCell(item.keywordKeys.join(", ") || "missing"),
|
|
item.needsHardStateReview ? "yes" : "",
|
|
]
|
|
.join(" | ")
|
|
.replace(/^/, "| ")} |`,
|
|
);
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function getProperty(node, name) {
|
|
return node.properties.find(
|
|
(property) => propertyName(property.name) === name,
|
|
);
|
|
}
|
|
|
|
function getPropertyInitializer(node, name) {
|
|
const property = getProperty(node, name);
|
|
if (!property) return undefined;
|
|
if (ts.isPropertyAssignment(property)) return property.initializer;
|
|
if (ts.isShorthandPropertyAssignment(property)) return property.name;
|
|
if (ts.isMethodDeclaration(property)) return property.body;
|
|
return undefined;
|
|
}
|
|
|
|
function getStringProperty(node, name) {
|
|
const initializer = getPropertyInitializer(node, name);
|
|
if (!initializer) return undefined;
|
|
if (ts.isStringLiteralLike(initializer)) return initializer.text;
|
|
return undefined;
|
|
}
|
|
|
|
function getStringArrayProperty(node, name) {
|
|
const initializer = getPropertyInitializer(node, name);
|
|
if (!initializer) return [];
|
|
if (ts.isIdentifier(initializer)) {
|
|
return CONTEXT_CONSTANTS[initializer.text] ?? [];
|
|
}
|
|
if (!ts.isArrayLiteralExpression(initializer)) return [];
|
|
return initializer.elements.flatMap((element) => {
|
|
if (ts.isStringLiteralLike(element)) return [element.text];
|
|
if (
|
|
ts.isSpreadElement(element) &&
|
|
ts.isIdentifier(element.expression) &&
|
|
CONTEXT_CONSTANTS[element.expression.text]
|
|
) {
|
|
return CONTEXT_CONSTANTS[element.expression.text];
|
|
}
|
|
return [];
|
|
});
|
|
}
|
|
|
|
function propertyName(name) {
|
|
if (!name) return undefined;
|
|
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
|
|
return undefined;
|
|
}
|
|
|
|
function matchAll(text, patterns) {
|
|
return patterns.filter((pattern) => new RegExp(pattern).test(text));
|
|
}
|
|
|
|
function actionNameToKeywordStem(actionName) {
|
|
const words = String(actionName ?? "")
|
|
.trim()
|
|
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
.split(/[^A-Za-z0-9]+/g)
|
|
.map((word) => word.trim().toLowerCase())
|
|
.filter(Boolean);
|
|
if (words.length === 0) return "";
|
|
return [words[0], ...words.slice(1).map(capitalizeAscii)].join("");
|
|
}
|
|
|
|
function capitalizeAscii(value) {
|
|
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
}
|
|
|
|
function escapeCell(value) {
|
|
return String(value).replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
}
|