mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(scripts): dedupe guard checks and smoke helpers
This commit is contained in:
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import {
|
||||||
|
collectTypeScriptFiles,
|
||||||
|
getPropertyNameText,
|
||||||
|
resolveRepoRoot,
|
||||||
|
runAsScript,
|
||||||
|
toLine,
|
||||||
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
|
|
||||||
const acpCoreProtectedSources = [
|
const acpCoreProtectedSources = [
|
||||||
path.join(repoRoot, "src", "acp"),
|
path.join(repoRoot, "src", "acp"),
|
||||||
@@ -57,50 +63,6 @@ const comparisonOperators = new Set([
|
|||||||
|
|
||||||
const allowedViolations = new Set([]);
|
const allowedViolations = new Set([]);
|
||||||
|
|
||||||
function isTestLikeFile(filePath) {
|
|
||||||
return (
|
|
||||||
filePath.endsWith(".test.ts") ||
|
|
||||||
filePath.endsWith(".test-utils.ts") ||
|
|
||||||
filePath.endsWith(".test-harness.ts") ||
|
|
||||||
filePath.endsWith(".e2e-harness.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectTypeScriptFiles(targetPath) {
|
|
||||||
const stat = await fs.stat(targetPath);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
if (!targetPath.endsWith(".ts") || isTestLikeFile(targetPath)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [targetPath];
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
||||||
const files = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(targetPath, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
files.push(...(await collectTypeScriptFiles(entryPath)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entryPath.endsWith(".ts")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isTestLikeFile(entryPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
files.push(entryPath);
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toLine(sourceFile, node) {
|
|
||||||
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isChannelsPropertyAccess(node) {
|
function isChannelsPropertyAccess(node) {
|
||||||
if (ts.isPropertyAccessExpression(node)) {
|
if (ts.isPropertyAccessExpression(node)) {
|
||||||
return node.name.text === "channels";
|
return node.name.text === "channels";
|
||||||
@@ -130,13 +92,6 @@ function matchesChannelModuleSpecifier(specifier) {
|
|||||||
return channelSegmentRe.test(specifier.replaceAll("\\", "/"));
|
return channelSegmentRe.test(specifier.replaceAll("\\", "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPropertyNameText(name) {
|
|
||||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
||||||
return name.text;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userFacingChannelNameRe =
|
const userFacingChannelNameRe =
|
||||||
/\b(?:discord|telegram|slack|signal|imessage|whatsapp|google\s*chat|irc|line|zalo|matrix|msteams|bluebubbles)\b/i;
|
/\b(?:discord|telegram|slack|signal|imessage|whatsapp|google\s*chat|irc|line|zalo|matrix|msteams|bluebubbles)\b/i;
|
||||||
const systemMarkLiteral = "⚙️";
|
const systemMarkLiteral = "⚙️";
|
||||||
@@ -348,16 +303,12 @@ export async function main() {
|
|||||||
for (const ruleSet of boundaryRuleSets) {
|
for (const ruleSet of boundaryRuleSets) {
|
||||||
const files = (
|
const files = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ruleSet.sources.map(async (sourcePath) => {
|
ruleSet.sources.map(
|
||||||
try {
|
async (sourcePath) =>
|
||||||
return await collectTypeScriptFiles(sourcePath);
|
await collectTypeScriptFiles(sourcePath, {
|
||||||
} catch (error) {
|
ignoreMissing: true,
|
||||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
}),
|
||||||
return [];
|
),
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
).flat();
|
).flat();
|
||||||
for (const filePath of files) {
|
for (const filePath of files) {
|
||||||
@@ -389,17 +340,4 @@ export async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectExecution = (() => {
|
runAsScript(import.meta.url, main);
|
||||||
const entry = process.argv[1];
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isDirectExecution) {
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import {
|
||||||
|
collectFileViolations,
|
||||||
|
getPropertyNameText,
|
||||||
|
resolveRepoRoot,
|
||||||
|
runAsScript,
|
||||||
|
toLine,
|
||||||
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
||||||
|
|
||||||
const allowedFiles = new Set([
|
const allowedFiles = new Set([
|
||||||
@@ -31,43 +36,6 @@ const allowedResolverCallNames = new Set([
|
|||||||
"resolveIrcEffectiveAllowlists",
|
"resolveIrcEffectiveAllowlists",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isTestLikeFile(filePath) {
|
|
||||||
return (
|
|
||||||
filePath.endsWith(".test.ts") ||
|
|
||||||
filePath.endsWith(".test-utils.ts") ||
|
|
||||||
filePath.endsWith(".test-harness.ts") ||
|
|
||||||
filePath.endsWith(".e2e-harness.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectTypeScriptFiles(dir) {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
const out = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
out.push(...(await collectTypeScriptFiles(entryPath)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile() || !entryPath.endsWith(".ts") || isTestLikeFile(entryPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(entryPath);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toLine(sourceFile, node) {
|
|
||||||
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPropertyNameText(name) {
|
|
||||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
||||||
return name.text;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDeclarationNameText(name) {
|
function getDeclarationNameText(name) {
|
||||||
if (ts.isIdentifier(name)) {
|
if (ts.isIdentifier(name)) {
|
||||||
return name.text;
|
return name.text;
|
||||||
@@ -190,24 +158,12 @@ function findViolations(content, filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const files = (
|
const violations = await collectFileViolations({
|
||||||
await Promise.all(sourceRoots.map(async (root) => await collectTypeScriptFiles(root)))
|
sourceRoots,
|
||||||
).flat();
|
repoRoot,
|
||||||
|
findViolations,
|
||||||
const violations = [];
|
skipFile: (filePath) => allowedFiles.has(filePath),
|
||||||
for (const filePath of files) {
|
});
|
||||||
if (allowedFiles.has(filePath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const content = await fs.readFile(filePath, "utf8");
|
|
||||||
const fileViolations = findViolations(content, filePath);
|
|
||||||
for (const violation of fileViolations) {
|
|
||||||
violations.push({
|
|
||||||
path: path.relative(repoRoot, filePath),
|
|
||||||
...violation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violations.length === 0) {
|
if (violations.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -223,17 +179,4 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectExecution = (() => {
|
runAsScript(import.meta.url, main);
|
||||||
const entry = process.argv[1];
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isDirectExecution) {
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import {
|
||||||
|
collectTypeScriptFiles,
|
||||||
|
resolveRepoRoot,
|
||||||
|
runAsScript,
|
||||||
|
toLine,
|
||||||
|
unwrapExpression,
|
||||||
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
const sourceRoots = [
|
const sourceRoots = [
|
||||||
path.join(repoRoot, "src", "channels"),
|
path.join(repoRoot, "src", "channels"),
|
||||||
path.join(repoRoot, "src", "infra", "outbound"),
|
path.join(repoRoot, "src", "infra", "outbound"),
|
||||||
@@ -15,38 +21,6 @@ const sourceRoots = [
|
|||||||
];
|
];
|
||||||
const allowedCallsites = new Set([path.join(repoRoot, "extensions", "feishu", "src", "dedup.ts")]);
|
const allowedCallsites = new Set([path.join(repoRoot, "extensions", "feishu", "src", "dedup.ts")]);
|
||||||
|
|
||||||
function isTestLikeFile(filePath) {
|
|
||||||
return (
|
|
||||||
filePath.endsWith(".test.ts") ||
|
|
||||||
filePath.endsWith(".test-utils.ts") ||
|
|
||||||
filePath.endsWith(".test-harness.ts") ||
|
|
||||||
filePath.endsWith(".e2e-harness.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectTypeScriptFiles(dir) {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
const out = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
out.push(...(await collectTypeScriptFiles(entryPath)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entryPath.endsWith(".ts")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isTestLikeFile(entryPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(entryPath);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectOsTmpdirImports(sourceFile) {
|
function collectOsTmpdirImports(sourceFile) {
|
||||||
const osModuleSpecifiers = new Set(["node:os", "os"]);
|
const osModuleSpecifiers = new Set(["node:os", "os"]);
|
||||||
const osNamespaceOrDefault = new Set();
|
const osNamespaceOrDefault = new Set();
|
||||||
@@ -81,25 +55,6 @@ function collectOsTmpdirImports(sourceFile) {
|
|||||||
return { osNamespaceOrDefault, namedTmpdir };
|
return { osNamespaceOrDefault, namedTmpdir };
|
||||||
}
|
}
|
||||||
|
|
||||||
function unwrapExpression(expression) {
|
|
||||||
let current = expression;
|
|
||||||
while (true) {
|
|
||||||
if (ts.isParenthesizedExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ts.isNonNullExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
|
export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
|
||||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||||
const { osNamespaceOrDefault, namedTmpdir } = collectOsTmpdirImports(sourceFile);
|
const { osNamespaceOrDefault, namedTmpdir } = collectOsTmpdirImports(sourceFile);
|
||||||
@@ -114,11 +69,9 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
|
|||||||
ts.isIdentifier(callee.expression) &&
|
ts.isIdentifier(callee.expression) &&
|
||||||
osNamespaceOrDefault.has(callee.expression.text)
|
osNamespaceOrDefault.has(callee.expression.text)
|
||||||
) {
|
) {
|
||||||
const line = sourceFile.getLineAndCharacterOfPosition(callee.getStart(sourceFile)).line + 1;
|
lines.push(toLine(sourceFile, callee));
|
||||||
lines.push(line);
|
|
||||||
} else if (ts.isIdentifier(callee) && namedTmpdir.has(callee.text)) {
|
} else if (ts.isIdentifier(callee) && namedTmpdir.has(callee.text)) {
|
||||||
const line = sourceFile.getLineAndCharacterOfPosition(callee.getStart(sourceFile)).line + 1;
|
lines.push(toLine(sourceFile, callee));
|
||||||
lines.push(line);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ts.forEachChild(node, visit);
|
ts.forEachChild(node, visit);
|
||||||
@@ -130,7 +83,14 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
|
|||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const files = (
|
const files = (
|
||||||
await Promise.all(sourceRoots.map(async (dir) => await collectTypeScriptFiles(dir)))
|
await Promise.all(
|
||||||
|
sourceRoots.map(
|
||||||
|
async (dir) =>
|
||||||
|
await collectTypeScriptFiles(dir, {
|
||||||
|
ignoreMissing: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
).flat();
|
).flat();
|
||||||
const violations = [];
|
const violations = [];
|
||||||
|
|
||||||
@@ -158,17 +118,4 @@ export async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectExecution = (() => {
|
runAsScript(import.meta.url, main);
|
||||||
const entry = process.argv[1];
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isDirectExecution) {
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import {
|
||||||
|
collectTypeScriptFiles,
|
||||||
|
resolveRepoRoot,
|
||||||
|
runAsScript,
|
||||||
|
toLine,
|
||||||
|
unwrapExpression,
|
||||||
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
const sourceRoots = [
|
const sourceRoots = [
|
||||||
path.join(repoRoot, "src", "telegram"),
|
path.join(repoRoot, "src", "telegram"),
|
||||||
path.join(repoRoot, "src", "discord"),
|
path.join(repoRoot, "src", "discord"),
|
||||||
@@ -65,69 +71,6 @@ const allowedRawFetchCallsites = new Set([
|
|||||||
"src/slack/monitor/media.ts:108",
|
"src/slack/monitor/media.ts:108",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isTestLikeFile(filePath) {
|
|
||||||
return (
|
|
||||||
filePath.endsWith(".test.ts") ||
|
|
||||||
filePath.endsWith(".test-utils.ts") ||
|
|
||||||
filePath.endsWith(".test-harness.ts") ||
|
|
||||||
filePath.endsWith(".e2e-harness.ts") ||
|
|
||||||
filePath.endsWith(".browser.test.ts") ||
|
|
||||||
filePath.endsWith(".node.test.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectTypeScriptFiles(targetPath) {
|
|
||||||
const stat = await fs.stat(targetPath);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
if (!targetPath.endsWith(".ts") || isTestLikeFile(targetPath)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [targetPath];
|
|
||||||
}
|
|
||||||
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
||||||
const files = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(targetPath, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
if (entry.name === "node_modules") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
files.push(...(await collectTypeScriptFiles(entryPath)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entryPath.endsWith(".ts")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isTestLikeFile(entryPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
files.push(entryPath);
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapExpression(expression) {
|
|
||||||
let current = expression;
|
|
||||||
while (true) {
|
|
||||||
if (ts.isParenthesizedExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ts.isNonNullExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRawFetchCall(expression) {
|
function isRawFetchCall(expression) {
|
||||||
const callee = unwrapExpression(expression);
|
const callee = unwrapExpression(expression);
|
||||||
if (ts.isIdentifier(callee)) {
|
if (ts.isIdentifier(callee)) {
|
||||||
@@ -148,9 +91,7 @@ export function findRawFetchCallLines(content, fileName = "source.ts") {
|
|||||||
const lines = [];
|
const lines = [];
|
||||||
const visit = (node) => {
|
const visit = (node) => {
|
||||||
if (ts.isCallExpression(node) && isRawFetchCall(node.expression)) {
|
if (ts.isCallExpression(node) && isRawFetchCall(node.expression)) {
|
||||||
const line =
|
lines.push(toLine(sourceFile, node.expression));
|
||||||
sourceFile.getLineAndCharacterOfPosition(node.expression.getStart(sourceFile)).line + 1;
|
|
||||||
lines.push(line);
|
|
||||||
}
|
}
|
||||||
ts.forEachChild(node, visit);
|
ts.forEachChild(node, visit);
|
||||||
};
|
};
|
||||||
@@ -161,13 +102,13 @@ export function findRawFetchCallLines(content, fileName = "source.ts") {
|
|||||||
export async function main() {
|
export async function main() {
|
||||||
const files = (
|
const files = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sourceRoots.map(async (sourceRoot) => {
|
sourceRoots.map(
|
||||||
try {
|
async (sourceRoot) =>
|
||||||
return await collectTypeScriptFiles(sourceRoot);
|
await collectTypeScriptFiles(sourceRoot, {
|
||||||
} catch {
|
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],
|
||||||
return [];
|
ignoreMissing: true,
|
||||||
}
|
}),
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
).flat();
|
).flat();
|
||||||
|
|
||||||
@@ -198,17 +139,4 @@ export async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectExecution = (() => {
|
runAsScript(import.meta.url, main);
|
||||||
const entry = process.argv[1];
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isDirectExecution) {
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,63 +2,19 @@
|
|||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import {
|
||||||
|
collectTypeScriptFiles,
|
||||||
|
resolveRepoRoot,
|
||||||
|
runAsScript,
|
||||||
|
toLine,
|
||||||
|
unwrapExpression,
|
||||||
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
const uiSourceDir = path.join(repoRoot, "ui", "src", "ui");
|
const uiSourceDir = path.join(repoRoot, "ui", "src", "ui");
|
||||||
const allowedCallsites = new Set([path.join(uiSourceDir, "open-external-url.ts")]);
|
const allowedCallsites = new Set([path.join(uiSourceDir, "open-external-url.ts")]);
|
||||||
|
|
||||||
function isTestFile(filePath) {
|
|
||||||
return (
|
|
||||||
filePath.endsWith(".test.ts") ||
|
|
||||||
filePath.endsWith(".browser.test.ts") ||
|
|
||||||
filePath.endsWith(".node.test.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectTypeScriptFiles(dir) {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
const out = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
out.push(...(await collectTypeScriptFiles(entryPath)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entryPath.endsWith(".ts")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isTestFile(entryPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(entryPath);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapExpression(expression) {
|
|
||||||
let current = expression;
|
|
||||||
while (true) {
|
|
||||||
if (ts.isParenthesizedExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ts.isNonNullExpression(current)) {
|
|
||||||
current = current.expression;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function asPropertyAccess(expression) {
|
function asPropertyAccess(expression) {
|
||||||
if (ts.isPropertyAccessExpression(expression)) {
|
if (ts.isPropertyAccessExpression(expression)) {
|
||||||
return expression;
|
return expression;
|
||||||
@@ -87,9 +43,7 @@ export function findRawWindowOpenLines(content, fileName = "source.ts") {
|
|||||||
|
|
||||||
const visit = (node) => {
|
const visit = (node) => {
|
||||||
if (ts.isCallExpression(node) && isRawWindowOpenCall(node.expression)) {
|
if (ts.isCallExpression(node) && isRawWindowOpenCall(node.expression)) {
|
||||||
const line =
|
lines.push(toLine(sourceFile, node.expression));
|
||||||
sourceFile.getLineAndCharacterOfPosition(node.expression.getStart(sourceFile)).line + 1;
|
|
||||||
lines.push(line);
|
|
||||||
}
|
}
|
||||||
ts.forEachChild(node, visit);
|
ts.forEachChild(node, visit);
|
||||||
};
|
};
|
||||||
@@ -99,7 +53,10 @@ export function findRawWindowOpenLines(content, fileName = "source.ts") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const files = await collectTypeScriptFiles(uiSourceDir);
|
const files = await collectTypeScriptFiles(uiSourceDir, {
|
||||||
|
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],
|
||||||
|
ignoreMissing: true,
|
||||||
|
});
|
||||||
const violations = [];
|
const violations = [];
|
||||||
|
|
||||||
for (const filePath of files) {
|
for (const filePath of files) {
|
||||||
@@ -126,17 +83,4 @@ export async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectExecution = (() => {
|
runAsScript(import.meta.url, main);
|
||||||
const entry = process.argv[1];
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isDirectExecution) {
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,50 +1,18 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import {
|
||||||
|
collectFileViolations,
|
||||||
|
getPropertyNameText,
|
||||||
|
resolveRepoRoot,
|
||||||
|
runAsScript,
|
||||||
|
toLine,
|
||||||
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||||
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
||||||
|
|
||||||
function isTestLikeFile(filePath) {
|
|
||||||
return (
|
|
||||||
filePath.endsWith(".test.ts") ||
|
|
||||||
filePath.endsWith(".test-utils.ts") ||
|
|
||||||
filePath.endsWith(".test-harness.ts") ||
|
|
||||||
filePath.endsWith(".e2e-harness.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectTypeScriptFiles(dir) {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
const out = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
out.push(...(await collectTypeScriptFiles(entryPath)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile() || !entryPath.endsWith(".ts") || isTestLikeFile(entryPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(entryPath);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toLine(sourceFile, node) {
|
|
||||||
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPropertyNameText(name) {
|
|
||||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
||||||
return name.text;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUndefinedLikeExpression(node) {
|
function isUndefinedLikeExpression(node) {
|
||||||
if (ts.isIdentifier(node) && node.text === "undefined") {
|
if (ts.isIdentifier(node) && node.text === "undefined") {
|
||||||
return true;
|
return true;
|
||||||
@@ -114,21 +82,11 @@ function findViolations(content, filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const files = (
|
const violations = await collectFileViolations({
|
||||||
await Promise.all(sourceRoots.map(async (root) => await collectTypeScriptFiles(root)))
|
sourceRoots,
|
||||||
).flat();
|
repoRoot,
|
||||||
const violations = [];
|
findViolations,
|
||||||
|
});
|
||||||
for (const filePath of files) {
|
|
||||||
const content = await fs.readFile(filePath, "utf8");
|
|
||||||
const fileViolations = findViolations(content, filePath);
|
|
||||||
for (const violation of fileViolations) {
|
|
||||||
violations.push({
|
|
||||||
path: path.relative(repoRoot, filePath),
|
|
||||||
...violation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violations.length === 0) {
|
if (violations.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -141,17 +99,4 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectExecution = (() => {
|
runAsScript(import.meta.url, main);
|
||||||
const entry = process.argv[1];
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isDirectExecution) {
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -340,39 +340,17 @@ async function discordApi<T>(params: {
|
|||||||
body?: unknown;
|
body?: unknown;
|
||||||
retries?: number;
|
retries?: number;
|
||||||
}): Promise<T> {
|
}): Promise<T> {
|
||||||
const retries = params.retries ?? 6;
|
return requestDiscordJson<T>({
|
||||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
method: params.method,
|
||||||
const response = await fetch(`${DISCORD_API_BASE}${params.path}`, {
|
path: params.path,
|
||||||
method: params.method,
|
headers: {
|
||||||
headers: {
|
Authorization: params.authHeader,
|
||||||
Authorization: params.authHeader,
|
"Content-Type": "application/json",
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
},
|
body: params.body,
|
||||||
body: params.body === undefined ? undefined : JSON.stringify(params.body),
|
retries: params.retries,
|
||||||
});
|
errorPrefix: "Discord API",
|
||||||
|
});
|
||||||
if (response.status === 429) {
|
|
||||||
const body = (await response.json().catch(() => ({}))) as { retry_after?: number };
|
|
||||||
const waitSeconds = typeof body.retry_after === "number" ? body.retry_after : 1;
|
|
||||||
await sleep(Math.ceil(waitSeconds * 1000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text().catch(() => "");
|
|
||||||
throw new Error(
|
|
||||||
`Discord API ${params.method} ${params.path} failed: ${response.status} ${response.statusText}${text ? ` :: ${text}` : ""}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
return undefined as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await response.json()) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Discord API ${params.method} ${params.path} exceeded retry budget.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discordWebhookApi<T>(params: {
|
async function discordWebhookApi<T>(params: {
|
||||||
@@ -383,15 +361,33 @@ async function discordWebhookApi<T>(params: {
|
|||||||
query?: string;
|
query?: string;
|
||||||
retries?: number;
|
retries?: number;
|
||||||
}): Promise<T> {
|
}): Promise<T> {
|
||||||
const retries = params.retries ?? 6;
|
|
||||||
const suffix = params.query ? `?${params.query}` : "";
|
const suffix = params.query ? `?${params.query}` : "";
|
||||||
const path = `/webhooks/${encodeURIComponent(params.webhookId)}/${encodeURIComponent(params.webhookToken)}${suffix}`;
|
const path = `/webhooks/${encodeURIComponent(params.webhookId)}/${encodeURIComponent(params.webhookToken)}${suffix}`;
|
||||||
|
return requestDiscordJson<T>({
|
||||||
|
method: params.method,
|
||||||
|
path,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: params.body,
|
||||||
|
retries: params.retries,
|
||||||
|
errorPrefix: "Discord webhook API",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestDiscordJson<T>(params: {
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
body?: unknown;
|
||||||
|
retries?: number;
|
||||||
|
errorPrefix: string;
|
||||||
|
}): Promise<T> {
|
||||||
|
const retries = params.retries ?? 6;
|
||||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||||
const response = await fetch(`${DISCORD_API_BASE}${path}`, {
|
const response = await fetch(`${DISCORD_API_BASE}${params.path}`, {
|
||||||
method: params.method,
|
method: params.method,
|
||||||
headers: {
|
headers: params.headers,
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: params.body === undefined ? undefined : JSON.stringify(params.body),
|
body: params.body === undefined ? undefined : JSON.stringify(params.body),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -405,7 +401,7 @@ async function discordWebhookApi<T>(params: {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text().catch(() => "");
|
const text = await response.text().catch(() => "");
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Discord webhook API ${params.method} ${path} failed: ${response.status} ${response.statusText}${text ? ` :: ${text}` : ""}`,
|
`${params.errorPrefix} ${params.method} ${params.path} failed: ${response.status} ${response.statusText}${text ? ` :: ${text}` : ""}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +412,7 @@ async function discordWebhookApi<T>(params: {
|
|||||||
return (await response.json()) as T;
|
return (await response.json()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Discord webhook API ${params.method} ${path} exceeded retry budget.`);
|
throw new Error(`${params.errorPrefix} ${params.method} ${params.path} exceeded retry budget.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readThreadBindings(filePath: string): Promise<ThreadBindingRecord[]> {
|
async function readThreadBindings(filePath: string): Promise<ThreadBindingRecord[]> {
|
||||||
@@ -487,6 +483,24 @@ function toRecentMessageRow(message: DiscordMessage) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadParentRecentMessages(params: {
|
||||||
|
args: Args;
|
||||||
|
readAuthHeader: string;
|
||||||
|
}): Promise<DiscordMessage[]> {
|
||||||
|
if (params.args.driverMode === "openclaw") {
|
||||||
|
return await readMessagesWithOpenclaw({
|
||||||
|
openclawBin: params.args.openclawBin,
|
||||||
|
target: params.args.channelId,
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await discordApi<DiscordMessage[]>({
|
||||||
|
method: "GET",
|
||||||
|
path: `/channels/${encodeURIComponent(params.args.channelId)}/messages?limit=20`,
|
||||||
|
authHeader: params.readAuthHeader,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function printOutput(params: { json: boolean; payload: SuccessResult | FailureResult }) {
|
function printOutput(params: { json: boolean; payload: SuccessResult | FailureResult }) {
|
||||||
if (params.json) {
|
if (params.json) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@@ -714,18 +728,7 @@ async function run(): Promise<SuccessResult | FailureResult> {
|
|||||||
if (!winningBinding?.threadId || !winningBinding?.targetSessionKey) {
|
if (!winningBinding?.threadId || !winningBinding?.targetSessionKey) {
|
||||||
let parentRecent: DiscordMessage[] = [];
|
let parentRecent: DiscordMessage[] = [];
|
||||||
try {
|
try {
|
||||||
parentRecent =
|
parentRecent = await loadParentRecentMessages({ args, readAuthHeader });
|
||||||
args.driverMode === "openclaw"
|
|
||||||
? await readMessagesWithOpenclaw({
|
|
||||||
openclawBin: args.openclawBin,
|
|
||||||
target: args.channelId,
|
|
||||||
limit: 20,
|
|
||||||
})
|
|
||||||
: await discordApi<DiscordMessage[]>({
|
|
||||||
method: "GET",
|
|
||||||
path: `/channels/${encodeURIComponent(args.channelId)}/messages?limit=20`,
|
|
||||||
authHeader: readAuthHeader,
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
// Best effort diagnostics only.
|
// Best effort diagnostics only.
|
||||||
}
|
}
|
||||||
@@ -782,18 +785,7 @@ async function run(): Promise<SuccessResult | FailureResult> {
|
|||||||
if (!ackMessage) {
|
if (!ackMessage) {
|
||||||
let parentRecent: DiscordMessage[] = [];
|
let parentRecent: DiscordMessage[] = [];
|
||||||
try {
|
try {
|
||||||
parentRecent =
|
parentRecent = await loadParentRecentMessages({ args, readAuthHeader });
|
||||||
args.driverMode === "openclaw"
|
|
||||||
? await readMessagesWithOpenclaw({
|
|
||||||
openclawBin: args.openclawBin,
|
|
||||||
target: args.channelId,
|
|
||||||
limit: 20,
|
|
||||||
})
|
|
||||||
: await discordApi<DiscordMessage[]>({
|
|
||||||
method: "GET",
|
|
||||||
path: `/channels/${encodeURIComponent(args.channelId)}/messages?limit=20`,
|
|
||||||
authHeader: readAuthHeader,
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
// Best effort diagnostics only.
|
// Best effort diagnostics only.
|
||||||
}
|
}
|
||||||
|
|||||||
147
scripts/lib/ts-guard-utils.mjs
Normal file
147
scripts/lib/ts-guard-utils.mjs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import ts from "typescript";
|
||||||
|
|
||||||
|
const baseTestSuffixes = [".test.ts", ".test-utils.ts", ".test-harness.ts", ".e2e-harness.ts"];
|
||||||
|
|
||||||
|
export function resolveRepoRoot(importMetaUrl) {
|
||||||
|
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTestLikeTypeScriptFile(filePath, options = {}) {
|
||||||
|
const extraTestSuffixes = options.extraTestSuffixes ?? [];
|
||||||
|
return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectTypeScriptFiles(targetPath, options = {}) {
|
||||||
|
const includeTests = options.includeTests ?? false;
|
||||||
|
const extraTestSuffixes = options.extraTestSuffixes ?? [];
|
||||||
|
const skipNodeModules = options.skipNodeModules ?? true;
|
||||||
|
const ignoreMissing = options.ignoreMissing ?? false;
|
||||||
|
|
||||||
|
let stat;
|
||||||
|
try {
|
||||||
|
stat = await fs.stat(targetPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
ignoreMissing &&
|
||||||
|
error &&
|
||||||
|
typeof error === "object" &&
|
||||||
|
"code" in error &&
|
||||||
|
error.code === "ENOENT"
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isFile()) {
|
||||||
|
if (!targetPath.endsWith(".ts")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!includeTests && isTestLikeTypeScriptFile(targetPath, { extraTestSuffixes })) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [targetPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
||||||
|
const out = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryPath = path.join(targetPath, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (skipNodeModules && entry.name === "node_modules") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(...(await collectTypeScriptFiles(entryPath, options)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !entryPath.endsWith(".ts")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!includeTests && isTestLikeTypeScriptFile(entryPath, { extraTestSuffixes })) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(entryPath);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectFileViolations(params) {
|
||||||
|
const files = (
|
||||||
|
await Promise.all(
|
||||||
|
params.sourceRoots.map(
|
||||||
|
async (root) =>
|
||||||
|
await collectTypeScriptFiles(root, {
|
||||||
|
ignoreMissing: true,
|
||||||
|
extraTestSuffixes: params.extraTestSuffixes,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
for (const filePath of files) {
|
||||||
|
if (params.skipFile?.(filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const content = await fs.readFile(filePath, "utf8");
|
||||||
|
const fileViolations = params.findViolations(content, filePath);
|
||||||
|
for (const violation of fileViolations) {
|
||||||
|
violations.push({
|
||||||
|
path: path.relative(params.repoRoot, filePath),
|
||||||
|
...violation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toLine(sourceFile, node) {
|
||||||
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPropertyNameText(name) {
|
||||||
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
||||||
|
return name.text;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unwrapExpression(expression) {
|
||||||
|
let current = expression;
|
||||||
|
while (true) {
|
||||||
|
if (ts.isParenthesizedExpression(current)) {
|
||||||
|
current = current.expression;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
|
||||||
|
current = current.expression;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ts.isNonNullExpression(current)) {
|
||||||
|
current = current.expression;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDirectExecution(importMetaUrl) {
|
||||||
|
const entry = process.argv[1];
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return path.resolve(entry) === fileURLToPath(importMetaUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runAsScript(importMetaUrl, main) {
|
||||||
|
if (!isDirectExecution(importMetaUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,12 +6,45 @@ import path from "node:path";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh");
|
const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh");
|
||||||
|
const XCODE_PLIST_PATH = path.join("Library", "Preferences", "com.apple.dt.Xcode.plist");
|
||||||
|
|
||||||
|
const DEFAULTS_WITH_ACCOUNT_SCRIPT = `#!/usr/bin/env bash
|
||||||
|
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
||||||
|
echo '(identifier = "dev@example.com";)'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 0`;
|
||||||
|
|
||||||
async function writeExecutable(filePath: string, body: string): Promise<void> {
|
async function writeExecutable(filePath: string, body: string): Promise<void> {
|
||||||
await writeFile(filePath, body, "utf8");
|
await writeFile(filePath, body, "utf8");
|
||||||
chmodSync(filePath, 0o755);
|
chmodSync(filePath, 0o755);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupFixture(params?: {
|
||||||
|
provisioningProfiles?: Record<string, string>;
|
||||||
|
}): Promise<{ homeDir: string; binDir: string }> {
|
||||||
|
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
||||||
|
const binDir = path.join(homeDir, "bin");
|
||||||
|
await mkdir(binDir, { recursive: true });
|
||||||
|
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
||||||
|
await writeFile(path.join(homeDir, XCODE_PLIST_PATH), "");
|
||||||
|
|
||||||
|
const provisioningProfiles = params?.provisioningProfiles;
|
||||||
|
if (provisioningProfiles) {
|
||||||
|
const profilesDir = path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles");
|
||||||
|
await mkdir(profilesDir, { recursive: true });
|
||||||
|
for (const [name, body] of Object.entries(provisioningProfiles)) {
|
||||||
|
await writeFile(path.join(profilesDir, name), body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { homeDir, binDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeDefaultsWithSignedInAccount(binDir: string): Promise<void> {
|
||||||
|
await writeExecutable(path.join(binDir, "defaults"), DEFAULTS_WITH_ACCOUNT_SCRIPT);
|
||||||
|
}
|
||||||
|
|
||||||
function runScript(
|
function runScript(
|
||||||
homeDir: string,
|
homeDir: string,
|
||||||
extraEnv: Record<string, string> = {},
|
extraEnv: Record<string, string> = {},
|
||||||
@@ -47,33 +80,18 @@ function runScript(
|
|||||||
|
|
||||||
describe("scripts/ios-team-id.sh", () => {
|
describe("scripts/ios-team-id.sh", () => {
|
||||||
it("falls back to Xcode-managed provisioning profiles when preference teams are empty", async () => {
|
it("falls back to Xcode-managed provisioning profiles when preference teams are empty", async () => {
|
||||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
const { homeDir, binDir } = await setupFixture({
|
||||||
const binDir = path.join(homeDir, "bin");
|
provisioningProfiles: {
|
||||||
await mkdir(binDir, { recursive: true });
|
"one.mobileprovision": "stub",
|
||||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
},
|
||||||
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
|
|
||||||
recursive: true,
|
|
||||||
});
|
});
|
||||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
|
||||||
await writeFile(
|
|
||||||
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
|
|
||||||
"stub",
|
|
||||||
);
|
|
||||||
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "plutil"),
|
path.join(binDir, "plutil"),
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
echo '{}'`,
|
echo '{}'`,
|
||||||
);
|
);
|
||||||
await writeExecutable(
|
await writeDefaultsWithSignedInAccount(binDir);
|
||||||
path.join(binDir, "defaults"),
|
|
||||||
`#!/usr/bin/env bash
|
|
||||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
|
||||||
echo '(identifier = "dev@example.com";)'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 0`,
|
|
||||||
);
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "security"),
|
path.join(binDir, "security"),
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
@@ -101,11 +119,7 @@ exit 0`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => {
|
it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => {
|
||||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
const { homeDir, binDir } = await setupFixture();
|
||||||
const binDir = path.join(homeDir, "bin");
|
|
||||||
await mkdir(binDir, { recursive: true });
|
|
||||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
|
||||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
|
||||||
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "plutil"),
|
path.join(binDir, "plutil"),
|
||||||
@@ -135,37 +149,19 @@ exit 1`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("honors IOS_PREFERRED_TEAM_ID when multiple profile teams are available", async () => {
|
it("honors IOS_PREFERRED_TEAM_ID when multiple profile teams are available", async () => {
|
||||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
const { homeDir, binDir } = await setupFixture({
|
||||||
const binDir = path.join(homeDir, "bin");
|
provisioningProfiles: {
|
||||||
await mkdir(binDir, { recursive: true });
|
"one.mobileprovision": "stub1",
|
||||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
"two.mobileprovision": "stub2",
|
||||||
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
|
},
|
||||||
recursive: true,
|
|
||||||
});
|
});
|
||||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
|
||||||
await writeFile(
|
|
||||||
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
|
|
||||||
"stub1",
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "two.mobileprovision"),
|
|
||||||
"stub2",
|
|
||||||
);
|
|
||||||
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "plutil"),
|
path.join(binDir, "plutil"),
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
echo '{}'`,
|
echo '{}'`,
|
||||||
);
|
);
|
||||||
await writeExecutable(
|
await writeDefaultsWithSignedInAccount(binDir);
|
||||||
path.join(binDir, "defaults"),
|
|
||||||
`#!/usr/bin/env bash
|
|
||||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
|
||||||
echo '(identifier = "dev@example.com";)'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 0`,
|
|
||||||
);
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "security"),
|
path.join(binDir, "security"),
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
@@ -194,26 +190,14 @@ exit 0`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("matches preferred team IDs even when parser output uses CRLF line endings", async () => {
|
it("matches preferred team IDs even when parser output uses CRLF line endings", async () => {
|
||||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
const { homeDir, binDir } = await setupFixture();
|
||||||
const binDir = path.join(homeDir, "bin");
|
|
||||||
await mkdir(binDir, { recursive: true });
|
|
||||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
|
||||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
|
||||||
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "plutil"),
|
path.join(binDir, "plutil"),
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
echo '{}'`,
|
echo '{}'`,
|
||||||
);
|
);
|
||||||
await writeExecutable(
|
await writeDefaultsWithSignedInAccount(binDir);
|
||||||
path.join(binDir, "defaults"),
|
|
||||||
`#!/usr/bin/env bash
|
|
||||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
|
||||||
echo '(identifier = "dev@example.com";)'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 0`,
|
|
||||||
);
|
|
||||||
await writeExecutable(
|
await writeExecutable(
|
||||||
path.join(binDir, "fake-python"),
|
path.join(binDir, "fake-python"),
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
|
|||||||
Reference in New Issue
Block a user