mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(scripts): share guard runners and paged select UI
This commit is contained in:
@@ -6,15 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
import { showPagedSelectList } from "./ui/paged-select";
|
||||||
import {
|
|
||||||
Container,
|
|
||||||
Key,
|
|
||||||
matchesKey,
|
|
||||||
type SelectItem,
|
|
||||||
SelectList,
|
|
||||||
Text,
|
|
||||||
} from "@mariozechner/pi-tui";
|
|
||||||
|
|
||||||
interface FileInfo {
|
interface FileInfo {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -108,87 +100,17 @@ export default function (pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show file picker with SelectList
|
const items = files.map((file) => ({
|
||||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
value: file,
|
||||||
const container = new Container();
|
label: `${file.status} ${file.file}`,
|
||||||
|
}));
|
||||||
// Top border
|
await showPagedSelectList({
|
||||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
ctx,
|
||||||
|
title: " Select file to diff",
|
||||||
// Title
|
items,
|
||||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
|
onSelect: (item) => {
|
||||||
|
|
||||||
// Build select items with colored status
|
|
||||||
const items: SelectItem[] = files.map((f) => {
|
|
||||||
let statusColor: string;
|
|
||||||
switch (f.status) {
|
|
||||||
case "M":
|
|
||||||
statusColor = theme.fg("warning", f.status);
|
|
||||||
break;
|
|
||||||
case "A":
|
|
||||||
statusColor = theme.fg("success", f.status);
|
|
||||||
break;
|
|
||||||
case "D":
|
|
||||||
statusColor = theme.fg("error", f.status);
|
|
||||||
break;
|
|
||||||
case "?":
|
|
||||||
statusColor = theme.fg("muted", f.status);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
statusColor = theme.fg("dim", f.status);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: f,
|
|
||||||
label: `${statusColor} ${f.file}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleRows = Math.min(files.length, 15);
|
|
||||||
let currentIndex = 0;
|
|
||||||
|
|
||||||
const selectList = new SelectList(items, visibleRows, {
|
|
||||||
selectedPrefix: (t) => theme.fg("accent", t),
|
|
||||||
selectedText: (t) => t, // Keep existing colors
|
|
||||||
description: (t) => theme.fg("muted", t),
|
|
||||||
scrollInfo: (t) => theme.fg("dim", t),
|
|
||||||
noMatch: (t) => theme.fg("warning", t),
|
|
||||||
});
|
|
||||||
selectList.onSelect = (item) => {
|
|
||||||
void openSelected(item.value as FileInfo);
|
void openSelected(item.value as FileInfo);
|
||||||
};
|
},
|
||||||
selectList.onCancel = () => done();
|
|
||||||
selectList.onSelectionChange = (item) => {
|
|
||||||
currentIndex = items.indexOf(item);
|
|
||||||
};
|
|
||||||
container.addChild(selectList);
|
|
||||||
|
|
||||||
// Help text
|
|
||||||
container.addChild(
|
|
||||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bottom border
|
|
||||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
||||||
|
|
||||||
return {
|
|
||||||
render: (w) => container.render(w),
|
|
||||||
invalidate: () => container.invalidate(),
|
|
||||||
handleInput: (data) => {
|
|
||||||
// Add paging with left/right
|
|
||||||
if (matchesKey(data, Key.left)) {
|
|
||||||
// Page up - clamp to 0
|
|
||||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
|
||||||
selectList.setSelectedIndex(currentIndex);
|
|
||||||
} else if (matchesKey(data, Key.right)) {
|
|
||||||
// Page down - clamp to last
|
|
||||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
|
||||||
selectList.setSelectedIndex(currentIndex);
|
|
||||||
} else {
|
|
||||||
selectList.handleInput(data);
|
|
||||||
}
|
|
||||||
tui.requestRender();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,15 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
import { showPagedSelectList } from "./ui/paged-select";
|
||||||
import {
|
|
||||||
Container,
|
|
||||||
Key,
|
|
||||||
matchesKey,
|
|
||||||
type SelectItem,
|
|
||||||
SelectList,
|
|
||||||
Text,
|
|
||||||
} from "@mariozechner/pi-tui";
|
|
||||||
|
|
||||||
interface FileEntry {
|
interface FileEntry {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -113,82 +105,30 @@ export default function (pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show file picker with SelectList
|
const items = files.map((file) => {
|
||||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
const ops: string[] = [];
|
||||||
const container = new Container();
|
if (file.operations.has("read")) {
|
||||||
|
ops.push("R");
|
||||||
// Top border
|
}
|
||||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
if (file.operations.has("write")) {
|
||||||
|
ops.push("W");
|
||||||
// Title
|
}
|
||||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
|
if (file.operations.has("edit")) {
|
||||||
|
ops.push("E");
|
||||||
// Build select items with colored operations
|
}
|
||||||
const items: SelectItem[] = files.map((f) => {
|
|
||||||
const ops: string[] = [];
|
|
||||||
if (f.operations.has("read")) {
|
|
||||||
ops.push(theme.fg("muted", "R"));
|
|
||||||
}
|
|
||||||
if (f.operations.has("write")) {
|
|
||||||
ops.push(theme.fg("success", "W"));
|
|
||||||
}
|
|
||||||
if (f.operations.has("edit")) {
|
|
||||||
ops.push(theme.fg("warning", "E"));
|
|
||||||
}
|
|
||||||
const opsLabel = ops.join("");
|
|
||||||
return {
|
|
||||||
value: f,
|
|
||||||
label: `${opsLabel} ${f.path}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleRows = Math.min(files.length, 15);
|
|
||||||
let currentIndex = 0;
|
|
||||||
|
|
||||||
const selectList = new SelectList(items, visibleRows, {
|
|
||||||
selectedPrefix: (t) => theme.fg("accent", t),
|
|
||||||
selectedText: (t) => t, // Keep existing colors
|
|
||||||
description: (t) => theme.fg("muted", t),
|
|
||||||
scrollInfo: (t) => theme.fg("dim", t),
|
|
||||||
noMatch: (t) => theme.fg("warning", t),
|
|
||||||
});
|
|
||||||
selectList.onSelect = (item) => {
|
|
||||||
void openSelected(item.value as FileEntry);
|
|
||||||
};
|
|
||||||
selectList.onCancel = () => done();
|
|
||||||
selectList.onSelectionChange = (item) => {
|
|
||||||
currentIndex = items.indexOf(item);
|
|
||||||
};
|
|
||||||
container.addChild(selectList);
|
|
||||||
|
|
||||||
// Help text
|
|
||||||
container.addChild(
|
|
||||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bottom border
|
|
||||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
render: (w) => container.render(w),
|
value: file,
|
||||||
invalidate: () => container.invalidate(),
|
label: `${ops.join("")} ${file.path}`,
|
||||||
handleInput: (data) => {
|
|
||||||
// Add paging with left/right
|
|
||||||
if (matchesKey(data, Key.left)) {
|
|
||||||
// Page up - clamp to 0
|
|
||||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
|
||||||
selectList.setSelectedIndex(currentIndex);
|
|
||||||
} else if (matchesKey(data, Key.right)) {
|
|
||||||
// Page down - clamp to last
|
|
||||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
|
||||||
selectList.setSelectedIndex(currentIndex);
|
|
||||||
} else {
|
|
||||||
selectList.handleInput(data);
|
|
||||||
}
|
|
||||||
tui.requestRender();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
await showPagedSelectList({
|
||||||
|
ctx,
|
||||||
|
title: " Select file to open",
|
||||||
|
items,
|
||||||
|
onSelect: (item) => {
|
||||||
|
void openSelected(item.value as FileEntry);
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
82
.pi/extensions/ui/paged-select.ts
Normal file
82
.pi/extensions/ui/paged-select.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Key,
|
||||||
|
matchesKey,
|
||||||
|
type SelectItem,
|
||||||
|
SelectList,
|
||||||
|
Text,
|
||||||
|
} from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
|
type CustomUiContext = {
|
||||||
|
ui: {
|
||||||
|
custom: <T>(
|
||||||
|
render: (
|
||||||
|
tui: { requestRender: () => void },
|
||||||
|
theme: {
|
||||||
|
fg: (tone: string, text: string) => string;
|
||||||
|
bold: (text: string) => string;
|
||||||
|
},
|
||||||
|
kb: unknown,
|
||||||
|
done: () => void,
|
||||||
|
) => {
|
||||||
|
render: (width: number) => string;
|
||||||
|
invalidate: () => void;
|
||||||
|
handleInput: (data: string) => void;
|
||||||
|
},
|
||||||
|
) => Promise<T>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function showPagedSelectList(params: {
|
||||||
|
ctx: CustomUiContext;
|
||||||
|
title: string;
|
||||||
|
items: SelectItem[];
|
||||||
|
onSelect: (item: SelectItem) => void;
|
||||||
|
}): Promise<void> {
|
||||||
|
await params.ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
container.addChild(new Text(theme.fg("accent", theme.bold(params.title)), 0, 0));
|
||||||
|
|
||||||
|
const visibleRows = Math.min(params.items.length, 15);
|
||||||
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
const selectList = new SelectList(params.items, visibleRows, {
|
||||||
|
selectedPrefix: (text) => theme.fg("accent", text),
|
||||||
|
selectedText: (text) => text,
|
||||||
|
description: (text) => theme.fg("muted", text),
|
||||||
|
scrollInfo: (text) => theme.fg("dim", text),
|
||||||
|
noMatch: (text) => theme.fg("warning", text),
|
||||||
|
});
|
||||||
|
selectList.onSelect = (item) => params.onSelect(item);
|
||||||
|
selectList.onCancel = () => done();
|
||||||
|
selectList.onSelectionChange = (item) => {
|
||||||
|
currentIndex = params.items.indexOf(item);
|
||||||
|
};
|
||||||
|
container.addChild(selectList);
|
||||||
|
|
||||||
|
container.addChild(
|
||||||
|
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
||||||
|
);
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (width) => container.render(width),
|
||||||
|
invalidate: () => container.invalidate(),
|
||||||
|
handleInput: (data) => {
|
||||||
|
if (matchesKey(data, Key.left)) {
|
||||||
|
currentIndex = Math.max(0, currentIndex - visibleRows);
|
||||||
|
selectList.setSelectedIndex(currentIndex);
|
||||||
|
} else if (matchesKey(data, Key.right)) {
|
||||||
|
currentIndex = Math.min(params.items.length - 1, currentIndex + visibleRows);
|
||||||
|
selectList.setSelectedIndex(currentIndex);
|
||||||
|
} else {
|
||||||
|
selectList.handleInput(data);
|
||||||
|
}
|
||||||
|
tui.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,24 +1,22 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import path from "node:path";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import { createPairingGuardContext } from "./lib/pairing-guard-context.mjs";
|
||||||
import {
|
import {
|
||||||
collectFileViolations,
|
collectFileViolations,
|
||||||
getPropertyNameText,
|
getPropertyNameText,
|
||||||
resolveRepoRoot,
|
|
||||||
runAsScript,
|
runAsScript,
|
||||||
toLine,
|
toLine,
|
||||||
} from "./lib/ts-guard-utils.mjs";
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
const { repoRoot, sourceRoots, resolveFromRepo } = createPairingGuardContext(import.meta.url);
|
||||||
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
|
||||||
|
|
||||||
const allowedFiles = new Set([
|
const allowedFiles = new Set([
|
||||||
path.join(repoRoot, "src", "security", "dm-policy-shared.ts"),
|
resolveFromRepo("src/security/dm-policy-shared.ts"),
|
||||||
path.join(repoRoot, "src", "channels", "allow-from.ts"),
|
resolveFromRepo("src/channels/allow-from.ts"),
|
||||||
// Config migration/audit logic may intentionally reference store + group fields.
|
// Config migration/audit logic may intentionally reference store + group fields.
|
||||||
path.join(repoRoot, "src", "security", "fix.ts"),
|
resolveFromRepo("src/security/fix.ts"),
|
||||||
path.join(repoRoot, "src", "security", "audit-channel.ts"),
|
resolveFromRepo("src/security/audit-channel.ts"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const storeIdentifierRe = /^(?:storeAllowFrom|storedAllowFrom|storeAllowList)$/i;
|
const storeIdentifierRe = /^(?:storeAllowFrom|storedAllowFrom|storeAllowList)$/i;
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
import {
|
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
|
||||||
collectTypeScriptFiles,
|
import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs";
|
||||||
resolveRepoRoot,
|
|
||||||
runAsScript,
|
|
||||||
toLine,
|
|
||||||
unwrapExpression,
|
|
||||||
} from "./lib/ts-guard-utils.mjs";
|
|
||||||
|
|
||||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
||||||
const sourceRoots = [
|
const sourceRoots = [
|
||||||
path.join(repoRoot, "src", "channels"),
|
"src/channels",
|
||||||
path.join(repoRoot, "src", "infra", "outbound"),
|
"src/infra/outbound",
|
||||||
path.join(repoRoot, "src", "line"),
|
"src/line",
|
||||||
path.join(repoRoot, "src", "media-understanding"),
|
"src/media-understanding",
|
||||||
path.join(repoRoot, "extensions"),
|
"extensions",
|
||||||
];
|
];
|
||||||
const allowedCallsites = new Set([path.join(repoRoot, "extensions", "feishu", "src", "dedup.ts")]);
|
const allowedRelativePaths = new Set(["extensions/feishu/src/dedup.ts"]);
|
||||||
|
|
||||||
function collectOsTmpdirImports(sourceFile) {
|
function collectOsTmpdirImports(sourceFile) {
|
||||||
const osModuleSpecifiers = new Set(["node:os", "os"]);
|
const osModuleSpecifiers = new Set(["node:os", "os"]);
|
||||||
@@ -82,40 +74,16 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const files = (
|
await runCallsiteGuard({
|
||||||
await Promise.all(
|
importMetaUrl: import.meta.url,
|
||||||
sourceRoots.map(
|
sourceRoots,
|
||||||
async (dir) =>
|
findCallLines: findMessagingTmpdirCallLines,
|
||||||
await collectTypeScriptFiles(dir, {
|
skipRelativePath: (relativePath) => allowedRelativePaths.has(relativePath),
|
||||||
ignoreMissing: true,
|
header: "Found os.tmpdir()/tmpdir() usage in messaging/channel runtime sources:",
|
||||||
}),
|
footer:
|
||||||
),
|
"Use resolvePreferredOpenClawTmpDir() or plugin-sdk temp helpers instead of host tmp defaults.",
|
||||||
)
|
sortViolations: false,
|
||||||
).flat();
|
});
|
||||||
const violations = [];
|
|
||||||
|
|
||||||
for (const filePath of files) {
|
|
||||||
if (allowedCallsites.has(filePath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const content = await fs.readFile(filePath, "utf8");
|
|
||||||
for (const line of findMessagingTmpdirCallLines(content, filePath)) {
|
|
||||||
violations.push(`${path.relative(repoRoot, filePath)}:${line}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violations.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Found os.tmpdir()/tmpdir() usage in messaging/channel runtime sources:");
|
|
||||||
for (const violation of violations) {
|
|
||||||
console.error(`- ${violation}`);
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
"Use resolvePreferredOpenClawTmpDir() or plugin-sdk temp helpers instead of host tmp defaults.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runAsScript(import.meta.url, main);
|
runAsScript(import.meta.url, main);
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { promises as fs } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
import {
|
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
|
||||||
collectTypeScriptFiles,
|
import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs";
|
||||||
resolveRepoRoot,
|
|
||||||
runAsScript,
|
|
||||||
toLine,
|
|
||||||
unwrapExpression,
|
|
||||||
} from "./lib/ts-guard-utils.mjs";
|
|
||||||
|
|
||||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
||||||
const sourceRoots = [
|
const sourceRoots = [
|
||||||
path.join(repoRoot, "src", "telegram"),
|
"src/telegram",
|
||||||
path.join(repoRoot, "src", "discord"),
|
"src/discord",
|
||||||
path.join(repoRoot, "src", "slack"),
|
"src/slack",
|
||||||
path.join(repoRoot, "src", "signal"),
|
"src/signal",
|
||||||
path.join(repoRoot, "src", "imessage"),
|
"src/imessage",
|
||||||
path.join(repoRoot, "src", "web"),
|
"src/web",
|
||||||
path.join(repoRoot, "src", "channels"),
|
"src/channels",
|
||||||
path.join(repoRoot, "src", "routing"),
|
"src/routing",
|
||||||
path.join(repoRoot, "src", "line"),
|
"src/line",
|
||||||
path.join(repoRoot, "extensions"),
|
"extensions",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Temporary allowlist for legacy callsites. New raw fetch callsites in channel/plugin runtime
|
// Temporary allowlist for legacy callsites. New raw fetch callsites in channel/plugin runtime
|
||||||
@@ -100,43 +92,15 @@ export function findRawFetchCallLines(content, fileName = "source.ts") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const files = (
|
await runCallsiteGuard({
|
||||||
await Promise.all(
|
importMetaUrl: import.meta.url,
|
||||||
sourceRoots.map(
|
sourceRoots,
|
||||||
async (sourceRoot) =>
|
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],
|
||||||
await collectTypeScriptFiles(sourceRoot, {
|
findCallLines: findRawFetchCallLines,
|
||||||
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],
|
allowCallsite: (callsite) => allowedRawFetchCallsites.has(callsite),
|
||||||
ignoreMissing: true,
|
header: "Found raw fetch() usage in channel/plugin runtime sources outside allowlist:",
|
||||||
}),
|
footer: "Use fetchWithSsrFGuard() or existing channel/plugin SDK wrappers for network calls.",
|
||||||
),
|
});
|
||||||
)
|
|
||||||
).flat();
|
|
||||||
|
|
||||||
const violations = [];
|
|
||||||
for (const filePath of files) {
|
|
||||||
const content = await fs.readFile(filePath, "utf8");
|
|
||||||
const relPath = path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
|
|
||||||
for (const line of findRawFetchCallLines(content, filePath)) {
|
|
||||||
const callsite = `${relPath}:${line}`;
|
|
||||||
if (allowedRawFetchCallsites.has(callsite)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
violations.push(callsite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violations.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Found raw fetch() usage in channel/plugin runtime sources outside allowlist:");
|
|
||||||
for (const violation of violations.toSorted()) {
|
|
||||||
console.error(`- ${violation}`);
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
"Use fetchWithSsrFGuard() or existing channel/plugin SDK wrappers for network calls.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runAsScript(import.meta.url, main);
|
runAsScript(import.meta.url, main);
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import path from "node:path";
|
|
||||||
import ts from "typescript";
|
import ts from "typescript";
|
||||||
|
import { createPairingGuardContext } from "./lib/pairing-guard-context.mjs";
|
||||||
import {
|
import {
|
||||||
collectFileViolations,
|
collectFileViolations,
|
||||||
getPropertyNameText,
|
getPropertyNameText,
|
||||||
resolveRepoRoot,
|
|
||||||
runAsScript,
|
runAsScript,
|
||||||
toLine,
|
toLine,
|
||||||
} from "./lib/ts-guard-utils.mjs";
|
} from "./lib/ts-guard-utils.mjs";
|
||||||
|
|
||||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
const { repoRoot, sourceRoots } = createPairingGuardContext(import.meta.url);
|
||||||
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
|
||||||
|
|
||||||
function isUndefinedLikeExpression(node) {
|
function isUndefinedLikeExpression(node) {
|
||||||
if (ts.isIdentifier(node) && node.text === "undefined") {
|
if (ts.isIdentifier(node) && node.text === "undefined") {
|
||||||
|
|||||||
45
scripts/lib/callsite-guard.mjs
Normal file
45
scripts/lib/callsite-guard.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import {
|
||||||
|
collectTypeScriptFilesFromRoots,
|
||||||
|
resolveRepoRoot,
|
||||||
|
resolveSourceRoots,
|
||||||
|
} from "./ts-guard-utils.mjs";
|
||||||
|
|
||||||
|
export async function runCallsiteGuard(params) {
|
||||||
|
const repoRoot = resolveRepoRoot(params.importMetaUrl);
|
||||||
|
const sourceRoots = resolveSourceRoots(repoRoot, params.sourceRoots);
|
||||||
|
const files = await collectTypeScriptFilesFromRoots(sourceRoots, {
|
||||||
|
extraTestSuffixes: params.extraTestSuffixes,
|
||||||
|
});
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
for (const filePath of files) {
|
||||||
|
const relPath = path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
|
||||||
|
if (params.skipRelativePath?.(relPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const content = await fs.readFile(filePath, "utf8");
|
||||||
|
for (const line of params.findCallLines(content, filePath)) {
|
||||||
|
const callsite = `${relPath}:${line}`;
|
||||||
|
if (params.allowCallsite?.(callsite)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
violations.push(callsite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(params.header);
|
||||||
|
const output = params.sortViolations === false ? violations : violations.toSorted();
|
||||||
|
for (const violation of output) {
|
||||||
|
console.error(`- ${violation}`);
|
||||||
|
}
|
||||||
|
if (params.footer) {
|
||||||
|
console.error(params.footer);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
13
scripts/lib/pairing-guard-context.mjs
Normal file
13
scripts/lib/pairing-guard-context.mjs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { resolveRepoRoot, resolveSourceRoots } from "./ts-guard-utils.mjs";
|
||||||
|
|
||||||
|
export function createPairingGuardContext(importMetaUrl) {
|
||||||
|
const repoRoot = resolveRepoRoot(importMetaUrl);
|
||||||
|
const sourceRoots = resolveSourceRoots(repoRoot, ["src", "extensions"]);
|
||||||
|
return {
|
||||||
|
repoRoot,
|
||||||
|
sourceRoots,
|
||||||
|
resolveFromRepo: (relativePath) =>
|
||||||
|
path.join(repoRoot, ...relativePath.split("/").filter(Boolean)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@ export function resolveRepoRoot(importMetaUrl) {
|
|||||||
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
|
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveSourceRoots(repoRoot, relativeRoots) {
|
||||||
|
return relativeRoots.map((root) => path.join(repoRoot, ...root.split("/").filter(Boolean)));
|
||||||
|
}
|
||||||
|
|
||||||
export function isTestLikeTypeScriptFile(filePath, options = {}) {
|
export function isTestLikeTypeScriptFile(filePath, options = {}) {
|
||||||
const extraTestSuffixes = options.extraTestSuffixes ?? [];
|
const extraTestSuffixes = options.extraTestSuffixes ?? [];
|
||||||
return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix));
|
return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix));
|
||||||
@@ -68,18 +72,24 @@ export async function collectTypeScriptFiles(targetPath, options = {}) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function collectFileViolations(params) {
|
export async function collectTypeScriptFilesFromRoots(sourceRoots, options = {}) {
|
||||||
const files = (
|
return (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
params.sourceRoots.map(
|
sourceRoots.map(
|
||||||
async (root) =>
|
async (root) =>
|
||||||
await collectTypeScriptFiles(root, {
|
await collectTypeScriptFiles(root, {
|
||||||
ignoreMissing: true,
|
ignoreMissing: true,
|
||||||
extraTestSuffixes: params.extraTestSuffixes,
|
...options,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
).flat();
|
).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectFileViolations(params) {
|
||||||
|
const files = await collectTypeScriptFilesFromRoots(params.sourceRoots, {
|
||||||
|
extraTestSuffixes: params.extraTestSuffixes,
|
||||||
|
});
|
||||||
|
|
||||||
const violations = [];
|
const violations = [];
|
||||||
for (const filePath of files) {
|
for (const filePath of files) {
|
||||||
|
|||||||
Reference in New Issue
Block a user