mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 19:52:50 +03:00
Merge release/v3.8.50 into fix/release-v3.8.50-basereds-2 (base versions win — #10198 landed the newer fixes; dead cachePolicy param dropped)
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
try {
|
||||
console.log("Fetching workflow runs...");
|
||||
const output = execSync("gh run list --limit 100 --json status,conclusion,databaseId", {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const runs = JSON.parse(output);
|
||||
|
||||
console.log(`Found ${runs.length} runs.`);
|
||||
let count = 0;
|
||||
for (const run of runs) {
|
||||
if (run.conclusion !== "success") {
|
||||
console.log(`Deleting run ID ${run.databaseId} with conclusion '${run.conclusion}'...`);
|
||||
try {
|
||||
execSync(`gh run delete ${run.databaseId}`);
|
||||
count++;
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete run ID ${run.databaseId}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Deleted ${count} runs successfully.`);
|
||||
} catch (error) {
|
||||
console.error("Error executing script:", error);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const REPO = "diegosouzapw/OmniRoute";
|
||||
const artifactsDir =
|
||||
process.env.ARTIFACTS_DIR ||
|
||||
path.join(process.cwd(), "artifacts");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 1. Get PR numbers
|
||||
console.log("Fetching open PR numbers...");
|
||||
const prNumbersOutput = execSync(
|
||||
`gh pr list --repo ${REPO} --state open --limit 500 --json number --jq '.[].number'`,
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
const prNumbers = prNumbersOutput.trim().split("\n").map(Number).filter(Boolean);
|
||||
console.log(`Found ${prNumbers.length} open PRs:`, prNumbers);
|
||||
|
||||
if (!fs.existsSync(artifactsDir)) {
|
||||
fs.mkdirSync(artifactsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 2. Fetch metadata and diff for each PR
|
||||
for (const prNum of prNumbers) {
|
||||
console.log(`\n--- Fetching PR #${prNum} ---`);
|
||||
|
||||
// Metadata
|
||||
try {
|
||||
const metadataCmd = `gh pr view ${prNum} --repo ${REPO} --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`;
|
||||
const metadataJson = execSync(metadataCmd, { encoding: "utf-8" });
|
||||
const metadataPath = path.join(artifactsDir, `pr_${prNum}_meta.json`);
|
||||
fs.writeFileSync(metadataPath, metadataJson);
|
||||
console.log(`Saved metadata to ${metadataPath}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch metadata for PR #${prNum}:`, err.message);
|
||||
}
|
||||
|
||||
// Diff
|
||||
try {
|
||||
const diffCmd = `gh pr diff ${prNum} --repo ${REPO}`;
|
||||
const diffText = execSync(diffCmd, { encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 });
|
||||
const diffPath = path.join("/tmp", `pr${prNum}.diff`);
|
||||
fs.writeFileSync(diffPath, diffText);
|
||||
console.log(`Saved diff to ${diffPath} (Size: ${diffText.length} bytes)`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch diff for PR #${prNum}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nAll PR data fetched successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error during PR fetching:", error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,280 +0,0 @@
|
||||
import fs from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
||||
|
||||
const filesToCheckoutOurs = [
|
||||
".source/browser.ts",
|
||||
".source/server.ts",
|
||||
"package-lock.json",
|
||||
"electron/package-lock.json",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/page.tsx",
|
||||
"src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx",
|
||||
"src/lib/db/contextHandoffs.ts",
|
||||
"src/app/api/keys/groups/[id]/keys/route.ts",
|
||||
"src/app/api/keys/groups/[id]/permissions/route.ts",
|
||||
"src/app/api/keys/groups/[id]/route.ts",
|
||||
"src/app/api/keys/groups/route.ts",
|
||||
"src/app/api/middleware/hooks/[name]/route.ts",
|
||||
"src/app/api/middleware/hooks/route.ts",
|
||||
"src/app/api/relay/tokens/[id]/route.ts",
|
||||
"src/app/api/relay/tokens/route.ts",
|
||||
"src/app/api/playground/simulate-route/route.ts",
|
||||
];
|
||||
|
||||
function runCmd(cmd) {
|
||||
console.log(`Running: ${cmd}`);
|
||||
return execSync(cmd, { cwd: projectRoot, encoding: "utf-8" });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Checkout ours for the files where HEAD is the preferred up-to-date state
|
||||
for (const file of filesToCheckoutOurs) {
|
||||
try {
|
||||
runCmd(`git checkout --ours "${file}"`);
|
||||
runCmd(`git add "${file}"`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to checkout --ours for ${file}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Resolve .dockerignore (keep release/v3.8.4 doc rules)
|
||||
try {
|
||||
runCmd("git checkout --theirs .dockerignore");
|
||||
runCmd("git add .dockerignore");
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve .dockerignore:", err.message);
|
||||
}
|
||||
|
||||
// 3. Resolve docs/reference/ENVIRONMENT.md (keep release/v3.8.4 table formatting)
|
||||
try {
|
||||
runCmd("git checkout --theirs docs/reference/ENVIRONMENT.md");
|
||||
runCmd("git add docs/reference/ENVIRONMENT.md");
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve docs/reference/ENVIRONMENT.md:", err.message);
|
||||
}
|
||||
|
||||
// 4. Resolve open-sse/executors/index.ts (keep both ClaudeWebExecutor and InnerAiExecutor)
|
||||
const execIndexFile = path.join(projectRoot, "open-sse/executors/index.ts");
|
||||
if (fs.existsSync(execIndexFile)) {
|
||||
let content = fs.readFileSync(execIndexFile, "utf-8");
|
||||
|
||||
// Resolve imports conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{ ClaudeWebExecutor \} from "\.\/claude-web\.ts";\r?\n=======\r?\nimport \{ InnerAiExecutor \} from "\.\/inner-ai\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { ClaudeWebExecutor } from "./claude-web.ts";\nimport { InnerAiExecutor } from "./inner-ai.ts";'
|
||||
);
|
||||
|
||||
// Resolve executor registration conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+"claude-web": new ClaudeWebExecutor\(\),\r?\n\s+"cw-web": new ClaudeWebExecutor\(\), \/\/ Alias\r?\n=======\r?\n\s+"inner-ai": new InnerAiExecutor\(\),\r?\n\s+"in-ai": new InnerAiExecutor\(\), \/\/ Alias\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' "claude-web": new ClaudeWebExecutor(),\n "cw-web": new ClaudeWebExecutor(), // Alias\n "inner-ai": new InnerAiExecutor(),\n "in-ai": new InnerAiExecutor(), // Alias'
|
||||
);
|
||||
|
||||
fs.writeFileSync(execIndexFile, content);
|
||||
runCmd("git add open-sse/executors/index.ts");
|
||||
}
|
||||
|
||||
// 7. Resolve src/app/api/providers/[id]/models/route.ts (combine imports)
|
||||
const modelsRoute = path.join(projectRoot, "src/app/api/providers/[id]/models/route.ts");
|
||||
if (fs.existsSync(modelsRoute)) {
|
||||
let content = fs.readFileSync(modelsRoute, "utf-8");
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\nimport \{ sanitizeErrorMessage \} from "@omniroute\/open-sse\/utils\/error";\r?\nimport \{ getStaticQoderModels \} from "@omniroute\/open-sse\/services\/qoderCli\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";\nimport { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";'
|
||||
);
|
||||
fs.writeFileSync(modelsRoute, content);
|
||||
runCmd("git add src/app/api/providers/[id]/models/route.ts");
|
||||
}
|
||||
|
||||
// 8. Resolve src/sse/handlers/chat.ts
|
||||
const sseChat = path.join(projectRoot, "src/sse/handlers/chat.ts");
|
||||
if (fs.existsSync(sseChat)) {
|
||||
let content = fs.readFileSync(sseChat, "utf-8");
|
||||
|
||||
// Resolve comment / modelStr conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\n\s+\/\/ `let` because the middleware-hook pipeline \(line ~319\) may reassign this\r?\n\s+\/\/ when a hook rewrites the target model\. Previously declared `const`, which\r?\n\s+\/\/ broke turbopack\/strict-mode builds \(PR #2670 regression\)\.\r?\n>>>>>>> release\/v3\.8\.4\r?\n\s+let modelStr = body\.model;/g,
|
||||
" // `let` because the middleware-hook pipeline (line ~319) may reassign this\n // when a hook rewrites the target model. Previously declared `const`, which\n // broke turbopack/strict-mode builds (PR [PR #2670](file:///home/diegosouzapw/dev/proxys/OmniRoute/package.json#L2670) regression).\n let modelStr = body.model;"
|
||||
);
|
||||
|
||||
// Resolve trafficType / modelAbortSignal conflict (1st occurrence)
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+trafficType\?: "production" \| "shadow";\r?\n=======\r?\n\s+modelAbortSignal\?: AbortSignal \| null;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' trafficType?: "production" | "shadow";\n modelAbortSignal?: AbortSignal | null;'
|
||||
);
|
||||
|
||||
fs.writeFileSync(sseChat, content);
|
||||
runCmd("git add src/sse/handlers/chat.ts");
|
||||
}
|
||||
|
||||
// 9. Resolve bin/cli/tray/autostart.mjs (keep execFileSync, combine ignoreFailure and systemd CI fallback)
|
||||
const autostart = path.join(projectRoot, "bin/cli/tray/autostart.mjs");
|
||||
if (fs.existsSync(autostart)) {
|
||||
let content = fs.readFileSync(autostart, "utf-8");
|
||||
|
||||
// runUserSystemctl conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+\} catch \{\r?\n=======\r?\n\s+\} catch \(err\) \{\r?\n\s+if \(!ignoreFailure\) throw err;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
` } catch (err) { \n if (!ignoreFailure) throw err;`
|
||||
);
|
||||
|
||||
// isSystemdServiceEnabled conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+return false;\r?\n=======\r?\n\s+\/\/ systemctl --user can't query the bus \(headless environments \/ CI runners\)\.\r?\n\s+\/\/ Treat the presence of the unit file as the source of truth, matching the\r?\n\s+\/\/ fallback used in enableLinux\(\) where unit-file existence counts as success\.\r?\n\s+return true;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
` // systemctl --user can't query the bus (headless environments / CI runners).\n // Treat the presence of the unit file as the source of truth, matching the\n // fallback used in enableLinux() where unit-file existence counts as success.\n return true;`
|
||||
);
|
||||
|
||||
fs.writeFileSync(autostart, content);
|
||||
runCmd("git add bin/cli/tray/autostart.mjs");
|
||||
}
|
||||
|
||||
// 10. Resolve electron/package.json
|
||||
const electronPkg = path.join(projectRoot, "electron/package.json");
|
||||
if (fs.existsSync(electronPkg)) {
|
||||
let content = fs.readFileSync(electronPkg, "utf-8");
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+"electron": "\^42\.2\.0",\r?\n\s+"electron-builder": "\^26\.11\.0"\r?\n=======\r?\n\s+"electron": "\^41\.2\.0",\r?\n\s+"electron-builder": "\^26\.11\.1"\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' "electron": "^42.2.0",\n "electron-builder": "^26.11.1"'
|
||||
);
|
||||
fs.writeFileSync(electronPkg, content);
|
||||
runCmd("git add electron/package.json");
|
||||
}
|
||||
|
||||
// 11. Resolve .github/workflows/ci.yml
|
||||
const ciYaml = path.join(projectRoot, ".github/workflows/ci.yml");
|
||||
if (fs.existsSync(ciYaml)) {
|
||||
let content = fs.readFileSync(ciYaml, "utf-8");
|
||||
|
||||
// Run c8 over shard title
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+rm -rf coverage-shard coverage-shard-report\r?\n=======\r?\n\s+# `--temp-directory` \(writable via NODE_V8_COVERAGE\) is what the merge\r?\n\s+# job reads with `c8 report --temp-directory \.\.\.`\. Using `--output-dir`\r?\n\s+# only produces the final json \*report\* and leaves the raw v8 files in\r?\n\s+# `coverage\/tmp`, so uploading `coverage-shard\/` was empty\. Pin the temp\r?\n\s+# dir so the raw coverage files live there and the artifact upload picks\r?\n\s+# them up regardless of `--test-force-exit` timing\.\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" rm -rf coverage-shard coverage-shard-report\n # `--temp-directory` (writable via NODE_V8_COVERAGE) is what the merge\n # job reads with `c8 report --temp-directory ...`. Using `--output-dir`\n # only produces the final json *report* and leaves the raw v8 files in\n # `coverage/tmp`, so uploading `coverage-shard/` was empty. Pin the temp\n # dir so the raw coverage files live there and the artifact upload picks\n # them up regardless of `--test-force-exit` timing."
|
||||
);
|
||||
|
||||
// c8 temp-directory arg
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\n\s+--temp-directory=coverage-shard\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" --temp-directory=coverage-shard"
|
||||
);
|
||||
|
||||
fs.writeFileSync(ciYaml, content);
|
||||
runCmd("git add .github/workflows/ci.yml");
|
||||
}
|
||||
|
||||
// 12. Resolve Dockerfile
|
||||
const dockerfile = path.join(projectRoot, "Dockerfile");
|
||||
if (fs.existsSync(dockerfile)) {
|
||||
let content = fs.readFileSync(dockerfile, "utf-8");
|
||||
|
||||
// FROM node
|
||||
content = content.replace(
|
||||
/FROM node:26\.2\.0-trixie-slim AS builder\r?\nFROM node:24-trixie-slim AS builder/g,
|
||||
"FROM node:24-trixie-slim AS builder"
|
||||
);
|
||||
|
||||
// apt-get cache mounts
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/var\/cache\/apt,sharing=locked \\\r?\n\s+--mount=type=cache,target=\/var\/lib\/apt\/lists,sharing=locked \\\r?\n\s+apt-get update \\\r?\n=======\r?\nRUN apt-get update \\\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
"RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\\n apt-get update \\"
|
||||
);
|
||||
|
||||
// npm ci script ignore and reproducible build check
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/root\/\.npm \\\r?\n\s+if \[ -f package-lock\.json \]; then \\\r?\n\s+npm ci --no-audit --no-fund --legacy-peer-deps; \\\r?\n\s+else \\\r?\n\s+npm install --no-audit --no-fund --legacy-peer-deps; \\\r?\n\s+fi\r?\n=======\r?\n# `--ignore-scripts` blocks the install\/postinstall hooks of dependencies,[\s\S]*?RUN npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
`# --ignore-scripts blocks the install/postinstall hooks of dependencies,
|
||||
# closing the supply-chain attack surface where a transitive dep can run
|
||||
# arbitrary code at install time. OmniRoute's own postinstall (
|
||||
# better-sqlite3 binary touchups, @swc/helpers copy) is only needed when
|
||||
# a packaged app/node_modules is unpacked — inside the Docker builder we
|
||||
# are doing a fresh native-platform install, so dropping the scripts is safe.
|
||||
#
|
||||
# We REQUIRE a committed package-lock.json so resolved dependency versions
|
||||
# are reproducible.
|
||||
RUN test -f package-lock.json \\
|
||||
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
|
||||
RUN --mount=type=cache,target=/root/.npm \\
|
||||
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts`
|
||||
);
|
||||
|
||||
// npm global install
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/root\/\.npm \\\r?\n\s+npm install -g --no-audit --no-fund @openai\/codex @anthropic-ai\/claude-code droid openclaw@latest\r?\n=======\r?\nRUN npm install -g --no-audit --no-fund @openai\/codex @anthropic-ai\/claude-code droid openclaw@latest\r?\n\r?\nUSER node\r?\n\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
"RUN --mount=type=cache,target=/root/.npm \\\n npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest\n\nUSER node"
|
||||
);
|
||||
|
||||
fs.writeFileSync(dockerfile, content);
|
||||
runCmd("git add Dockerfile");
|
||||
}
|
||||
|
||||
// 13. Resolve open-sse/services/combo.ts
|
||||
const openSseCombo = path.join(projectRoot, "open-sse/services/combo.ts");
|
||||
if (fs.existsSync(openSseCombo)) {
|
||||
let content = fs.readFileSync(openSseCombo, "utf-8");
|
||||
|
||||
// IntentClassifierConfig imports
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{\r?\n\s+classifyWithConfig,\r?\n\s+DEFAULT_INTENT_CONFIG,\r?\n\s+type IntentClassifierConfig,\r?\n\} from "\.\/intentClassifier\.ts";\r?\n=======\r?\nimport \{ notifyWebhookEvent \} from "\.\.\/\.\.\/src\/lib\/webhookDispatcher";\r?\nimport \{ classifyWithConfig, DEFAULT_INTENT_CONFIG \} from "\.\/intentClassifier\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { notifyWebhookEvent } from "../../src/lib/webhookDispatcher";\nimport {\n classifyWithConfig,\n DEFAULT_INTENT_CONFIG,\n type IntentClassifierConfig,\n} from "./intentClassifier.ts";'
|
||||
);
|
||||
|
||||
// handlePipelineCombo call
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+handleChatCore: handleSingleModel,\r?\n\s+log: \{\r?\n\s+info: log\.info,\r?\n\s+warn: log\.warn,\r?\n\s+error: log\.error \?\? log\.warn,\r?\n\s+\},\r?\n\s+settings: settings \?\? \{\},\r?\n\s+signal: signal \?\? undefined,\r?\n=======\r?\n\s+handleChatCore: handleSingleModelWithTimeout,\r?\n\s+log,\r?\n\s+settings,\r?\n\s+signal,\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" handleChatCore: handleSingleModelWithTimeout,\n log: {\n info: log.info,\n warn: log.warn,\n error: log.error ?? log.warn,\n },\n settings: settings ?? {},\n signal: signal ?? undefined,"
|
||||
);
|
||||
|
||||
// handleSingleModel call in loop
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const result = await handleSingleModelWrapped\(attemptBody, modelStr, \{\r?\n=======\r?\n\s+const result = await handleSingleModelWithTimeout\(body, modelStr, \{\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" const result = await handleSingleModelWithTimeout(attemptBody, modelStr, {"
|
||||
);
|
||||
|
||||
// recordSessionModelUsage conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+recordSessionModelUsage\([\s\S]*?\);\r?\n\s+\r?\n=======\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" recordSessionModelUsage(\n relayOptions.sessionId,\n combo.name,\n modelStr,\n provider,\n target.connectionId ?? undefined\n );"
|
||||
);
|
||||
|
||||
fs.writeFileSync(openSseCombo, content);
|
||||
runCmd("git add open-sse/services/combo.ts");
|
||||
}
|
||||
|
||||
// 14. Resolve src/app/api/copilot/chat/route.ts
|
||||
const copilotChatRoute = path.join(projectRoot, "src/app/api/copilot/chat/route.ts");
|
||||
if (fs.existsSync(copilotChatRoute)) {
|
||||
let content = fs.readFileSync(copilotChatRoute, "utf-8");
|
||||
|
||||
// Imports conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{ requireManagementAuth \} from "@\/lib\/api\/requireManagementAuth";\r?\nimport \{ processCopilotChat \} from "@\/lib\/copilot\/engine";\r?\nimport \{ isValidationFailure, validateBody \} from "@\/shared\/validation\/helpers";\r?\nimport \{ sanitizeErrorMessage \} from "@omniroute\/open-sse\/utils\/error\.ts";\r?\n=======\r?\nimport \{ processCopilotChat \} from "@\/lib\/copilot\/engine";\r?\nimport type \{ CopilotRequest \} from "@\/lib\/copilot\/engine";\r?\nimport \{ buildErrorBody \} from "@omniroute\/open-sse\/utils\/error";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { requireManagementAuth } from "@/lib/api/requireManagementAuth";\nimport { processCopilotChat } from "@/lib/copilot/engine";\nimport { isValidationFailure, validateBody } from "@/shared/validation/helpers";\nimport { sanitizeErrorMessage, buildErrorBody } from "@omniroute/open-sse/utils/error.ts";'
|
||||
);
|
||||
|
||||
// Schema content min length
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+content: z\.string\(\)\.min\(1, "message content is required"\),\r?\n=======\r?\n\s+content: z\.string\(\),\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' content: z.string().min(1, "message content is required"),'
|
||||
);
|
||||
|
||||
// POST implementation conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const authError = await requireManagementAuth\(request\);\r?\n\s+if \(authError\) return authError;\r?\n\r?\n\s+try \{\r?\n\s+const rawBody = await request.json\(\);\r?\n\s+const validation = validateBody\(copilotRequestSchema, rawBody\);\r?\n\s+if \(isValidationFailure\(validation\)\) \{\r?\n\s+return NextResponse\.json\(\{ error: validation\.error \}, \{ status: 400 \}\);\r?\n=======\r?\n\s+try \{\r?\n\s+const raw = await request.json\(\);\r?\n\s+const parsed = copilotRequestSchema\.safeParse\(raw\);\r?\n\s+if \(!parsed\.success\) \{\r?\n\s+return NextResponse\.json\r?\n\s+buildErrorBody\(400, parsed\.error\.issues\[0\]\?\.message \?\? "Invalid request"\),\r?\n\s+\{ status: 400 \}\r?\n\s+\);\r?\n>>>>>>> release\/v3\.8\.4\r?\n\s+\}\r?\n\s+const body = parsed\.data as CopilotRequest;\r?\n\r?\n\s+const response = await processCopilotChat\(body\);/g,
|
||||
" const authError = await requireManagementAuth(request);\n if (authError) return authError;\n\n try {\n const rawBody = await request.json();\n const validation = validateBody(copilotRequestSchema, rawBody);\n if (isValidationFailure(validation)) {\n return NextResponse.json(\n buildErrorBody(400, validation.error),\n { status: 400 }\n );\n }\n const response = await processCopilotChat(validation.data);"
|
||||
);
|
||||
|
||||
// Error handling conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const message = sanitizeErrorMessage\(error\);\r?\n\s+return NextResponse\.json\(\{ error: `Copilot error: \$\{message\}` \}, \{ status: 500 \}\);\r?\n=======\r?\n\s+\/\/ buildErrorBody\(\) routes through sanitizeErrorMessage\(\), which strips\r?\n\s+\/\/ stack traces and absolute file paths\. Hard rule #12\.\r?\n\s+const message = error instanceof Error \? error\.message : "Unknown error";\r?\n\s+return NextResponse\.json\(buildErrorBody\(500, message\), \{ status: 500 \}\);\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" const message = sanitizeErrorMessage(error);\n return NextResponse.json(buildErrorBody(500, `Copilot error: ${message}`), { status: 500 });"
|
||||
);
|
||||
|
||||
fs.writeFileSync(copilotChatRoute, content);
|
||||
runCmd("git add src/app/api/copilot/chat/route.ts");
|
||||
}
|
||||
|
||||
console.log("Resolutions written and staged!");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -36,7 +36,6 @@ const DOCS_ROOT = path.join(REPO_ROOT, "docs");
|
||||
const EXCLUDE_PREFIXES = [
|
||||
path.join(DOCS_ROOT, "i18n") + path.sep,
|
||||
path.join(DOCS_ROOT, "screenshots") + path.sep,
|
||||
path.join(DOCS_ROOT, "superpowers") + path.sep,
|
||||
path.join(DOCS_ROOT, "diagrams", "exported") + path.sep,
|
||||
];
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
// Exits 0 on success, 1 on STRICT drift (or any drift with --strict).
|
||||
// Run: node scripts/check/check-docs-counts-sync.mjs
|
||||
//
|
||||
// NOTE: the provider check trusts PROVIDER_REFERENCE.md as the canonical total. If a
|
||||
// provider is added to the code but the reference is not regenerated, this guard will
|
||||
// not catch it — regenerate with `npm run gen:provider-reference` before relying on it.
|
||||
// NOTE: PROVIDER_REFERENCE.md is no longer blindly trusted — a STRICT check compares
|
||||
// the doc's `Total providers` against the live provider modules (the same collections
|
||||
// the generator reads), so a hand-stale doc is a red, not a silently propagated total.
|
||||
// Fix by running `npm run gen:provider-reference`. Additional STRICT coverage added in
|
||||
// the 2026-08-12 hardening: llm.txt + package.json description (providers), migration
|
||||
// count (README/AGENTS/llm.txt), and canonical numbers inside the README SVG diagrams
|
||||
// (providers / MCP tools / routing strategies / free-tier pools).
|
||||
|
||||
import fs from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
@@ -76,6 +80,13 @@ export function readProviderTotal() {
|
||||
return parseProviderTotal(fs.readFileSync(abs, "utf8"));
|
||||
}
|
||||
|
||||
// STRICT: number of SQL migration files shipped with the app.
|
||||
export function countMigrations() {
|
||||
const abs = path.join(ROOT, "src", "lib", "db", "migrations");
|
||||
if (!fs.existsSync(abs)) return 0;
|
||||
return fs.readdirSync(abs).filter((f) => f.endsWith(".sql")).length;
|
||||
}
|
||||
|
||||
// STRICT: canonical i18n locale count, read from the shared config.
|
||||
export function countLocales() {
|
||||
const abs = path.join(ROOT, "config", "i18n.json");
|
||||
@@ -148,6 +159,15 @@ function readCodeFacts() {
|
||||
'import {notionTools} from "./open-sse/mcp-server/tools/notionTools.ts";',
|
||||
'import {obsidianTools} from "./open-sse/mcp-server/tools/obsidianTools.ts";',
|
||||
'import {compressionTools} from "./open-sse/mcp-server/tools/compressionTools.ts";',
|
||||
// Live provider total — the SAME collections gen-provider-reference.ts unions, so the
|
||||
// doc-vs-live check below cannot drift from the generator's definition of "provider".
|
||||
'import * as PROV from "./src/shared/constants/providers.ts";',
|
||||
"const provCols=[PROV.FREE_PROVIDERS,PROV.NOAUTH_PROVIDERS,PROV.OAUTH_PROVIDERS,",
|
||||
"PROV.WEB_COOKIE_PROVIDERS,PROV.APIKEY_PROVIDERS,PROV.LOCAL_PROVIDERS,PROV.SEARCH_PROVIDERS,",
|
||||
"PROV.AUDIO_ONLY_PROVIDERS,PROV.UPSTREAM_PROXY_PROVIDERS,PROV.CLOUD_AGENT_PROVIDERS,",
|
||||
"PROV.SYSTEM_PROVIDERS];",
|
||||
"const pids=new Set();",
|
||||
"for(const c of provCols)for(const p of Object.values(c||{}))if(p&&p.id)pids.add(p.id);",
|
||||
"const cols={MCP_TOOLS,memoryTools,skillTools,agentSkillTools,githubSkillTools,poolTools,",
|
||||
"gamificationTools,pluginTools,notionTools,obsidianTools,compressionTools};",
|
||||
"const sc=new Set();",
|
||||
@@ -158,7 +178,7 @@ function readCodeFacts() {
|
||||
'console.log("@@"+JSON.stringify({freeSteady:t.steadyRecurringTokens,',
|
||||
"freeFirst:t.firstMonthRealisticTokens,freePools:t.poolCount,engines:ENGINE_IDS.length,",
|
||||
"cliTotal:cli.length,cliCode:by('code'),cliAgent:by('agent'),",
|
||||
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size}));",
|
||||
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size}));",
|
||||
].join("");
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "docs-counts-"));
|
||||
try {
|
||||
@@ -252,6 +272,82 @@ export function makeNumberClaimValidator(expected, opts) {
|
||||
};
|
||||
}
|
||||
|
||||
// --- v3.8.50 hardening validators --------------------------------------------
|
||||
// PURE: doc total must equal the live provider-module total (closes the falso-verde
|
||||
// found in the 2026-08-12 audit: the doc sat hand-stale at 291 while the modules
|
||||
// defined 338, and every downstream check inherited the stale total).
|
||||
export function makeProviderReferenceValidator(expected) {
|
||||
return (content) => {
|
||||
const total = parseProviderTotal(content);
|
||||
if (!total) return { ok: false, detail: "no `Total providers: **N**` marker found" };
|
||||
if (total === expected)
|
||||
return { ok: true, detail: `doc total ${total} matches the live provider modules` };
|
||||
return {
|
||||
ok: false,
|
||||
detail:
|
||||
`doc total ${total} is stale — the live provider modules define ${expected} ` +
|
||||
`(run npm run gen:provider-reference)`,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// PURE: the npm package description must carry the live provider count.
|
||||
export function makePackageDescriptionValidator(expected) {
|
||||
return (content) => {
|
||||
let desc = "";
|
||||
try {
|
||||
desc = String(JSON.parse(content).description || "");
|
||||
} catch {
|
||||
return { ok: false, detail: "package.json could not be parsed" };
|
||||
}
|
||||
if (desc.includes(String(expected)))
|
||||
return { ok: true, detail: `description mentions the live provider count ${expected}` };
|
||||
return {
|
||||
ok: false,
|
||||
detail: `description does not mention the live provider count ${expected}: "${desc}"`,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// PURE: sweep an SVG's text/aria content for the canonical numbers. Patterns are
|
||||
// deliberately narrow — they anchor on the surrounding words so path coordinates,
|
||||
// width/font-size attributes and small unrelated counts ("15 providers ToS-flagged",
|
||||
// "100+ providers") can never register as claims. Providers require 3+ digits for the
|
||||
// same reason.
|
||||
const SVG_CANONICAL_PATTERNS = [
|
||||
{ key: "providers", what: "providers", pattern: /(\d{3,4}) (?:AI )?providers\b/g },
|
||||
{ key: "mcpTools", what: "MCP tools", pattern: /MCP (?:server with |with |\()(\d+)/g },
|
||||
{ key: "strategies", what: "routing strategies", pattern: /(\d+) routing strategies\b/g },
|
||||
{ key: "pools", what: "free-tier pools", pattern: /(\d+) provider pools\b/g },
|
||||
];
|
||||
|
||||
export function checkSvgCanonicalNumbers(content, expected) {
|
||||
const stale = [];
|
||||
let claims = 0;
|
||||
for (const { key, what, pattern } of SVG_CANONICAL_PATTERNS) {
|
||||
if (expected[key] == null) continue;
|
||||
for (const m of content.matchAll(pattern)) {
|
||||
claims++;
|
||||
const value = Number(m[1]);
|
||||
if (value !== expected[key]) stale.push(`"${m[0]}" (${what} — code has ${expected[key]})`);
|
||||
}
|
||||
}
|
||||
if (!claims) return { ok: true, detail: "no canonical-number claims in this SVG" };
|
||||
if (!stale.length) return { ok: true, detail: `${claims} canonical claim(s) match the code` };
|
||||
return { ok: false, detail: `stale: ${[...new Set(stale)].join(", ")}` };
|
||||
}
|
||||
|
||||
// The README-embedded diagrams that historically rotted because no gate read them
|
||||
// (the alt-text in README.md is checked, the SVG text nodes never were).
|
||||
const SVG_DIAGRAM_FILES = [
|
||||
"docs/diagrams/readme-hero.svg",
|
||||
"docs/diagrams/free-tier-budget.svg",
|
||||
"docs/diagrams/promise-pillars.svg",
|
||||
"docs/diagrams/comparison-table.svg",
|
||||
"docs/diagrams/cli-terminal.svg",
|
||||
"docs/diagrams/tier-cascade.svg",
|
||||
];
|
||||
|
||||
export function buildChecks() {
|
||||
return [
|
||||
{
|
||||
@@ -259,7 +355,26 @@ export function buildChecks() {
|
||||
actual: readProviderTotal(),
|
||||
docKey: "providers",
|
||||
strict: true,
|
||||
files: ["README.md", "AGENTS.md"],
|
||||
files: ["README.md", "AGENTS.md", "llm.txt"],
|
||||
},
|
||||
{
|
||||
label: "Provider count (package.json description)",
|
||||
actual: readProviderTotal(),
|
||||
docKey: "providers",
|
||||
strict: true,
|
||||
files: ["package.json"],
|
||||
validate: makePackageDescriptionValidator(readProviderTotal()),
|
||||
},
|
||||
{
|
||||
label: "DB migrations count",
|
||||
actual: countMigrations(),
|
||||
docKey: "migrations",
|
||||
strict: true,
|
||||
files: ["README.md", "AGENTS.md", "llm.txt"],
|
||||
validate: makeNumberClaimValidator(countMigrations(), {
|
||||
what: "migrations",
|
||||
pattern: /(\d+)\+? migrations?\b/gi,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "i18n locales count",
|
||||
@@ -289,6 +404,30 @@ export function buildChecks() {
|
||||
validate: makeNumberClaimValidator(expected, { what, ...opts }),
|
||||
});
|
||||
return [
|
||||
{
|
||||
label: "Provider reference total (doc vs live modules)",
|
||||
actual: f.providers,
|
||||
docKey: "providers (live)",
|
||||
strict: true,
|
||||
files: ["docs/reference/PROVIDER_REFERENCE.md"],
|
||||
validate: makeProviderReferenceValidator(f.providers),
|
||||
},
|
||||
{
|
||||
label: "SVG canonical numbers (live code)",
|
||||
actual:
|
||||
`${f.providers} providers / ${f.mcpTools} MCP tools / ` +
|
||||
`${countRoutingStrategies()} strategies / ${f.freePools} pools`,
|
||||
docKey: "SVG canonical numbers",
|
||||
strict: true,
|
||||
files: SVG_DIAGRAM_FILES,
|
||||
validate: (content) =>
|
||||
checkSvgCanonicalNumbers(content, {
|
||||
providers: f.providers,
|
||||
mcpTools: f.mcpTools,
|
||||
strategies: countRoutingStrategies(),
|
||||
pools: f.freePools,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Free-tier headline (live catalog)",
|
||||
actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools`,
|
||||
|
||||
@@ -363,19 +363,6 @@ const SKIP_DOC_FILES = new Set([
|
||||
"docs/reference/PROVIDER_REFERENCE.md", // auto-generated from providers.ts
|
||||
"docs/openapi.yaml",
|
||||
"docs/i18n", // translations — separate workflow
|
||||
// Design / research / plan docs: by definition describe not-yet-built files and
|
||||
// proposed (not-yet-shipped) endpoints (each carries a `Status: Design`/`Active
|
||||
// research`/`Plano` header). Same rationale as the audit report above — these are
|
||||
// forward-looking specs, not living API docs, so their forward references are
|
||||
// expected, not fabrications.
|
||||
"docs/research", // DISCOVERY_TOOL_DESIGN.md, UNLIMITED_LLM_ACCESS.md, …
|
||||
"docs/superpowers/plans", // dated implementation plans (files described before they exist)
|
||||
"docs/superpowers/specs", // dated research/spec reports (point-in-time findings, may cite proposed/not-yet-built endpoints, env vars, and files) — same rationale as the plans/research dirs above
|
||||
// Release notes are historical, point-in-time records: they intentionally describe
|
||||
// modules/paths as they were at that release (e.g. a module later moved or renamed).
|
||||
// Rewriting them to today's layout would falsify history — out of scope for a
|
||||
// living-docs accuracy gate.
|
||||
"docs/releases",
|
||||
// Forward-looking coverage plan: a `- [ ]` checklist of test targets and helper
|
||||
// components to be created. Same rationale as the design/plan docs above.
|
||||
"docs/ops/COVERAGE_PLAN.md",
|
||||
|
||||
@@ -10,14 +10,39 @@
|
||||
// - coverage/ — relatórios de cobertura gerados pelo c8
|
||||
// - quality-metrics.json — saída do collect-metrics.mjs (gerado, não-versionado)
|
||||
// - symlinks rastreados (mode 120000) — indício de `git add -A` em worktree
|
||||
// - _tasks (exato E prefixo) — repo git SEPARADO; o blob symlink rastreado causou DOIS
|
||||
// wipes do diretório real (2026-08-08 e 2026-08-10; Hard Rule #23)
|
||||
// - _references/ _mono_repo/ _ideia/ _cache/ — diretórios privados de raiz (regra /_*/)
|
||||
// - .claude/worktrees/ — worktrees de sessão nunca entram no repo
|
||||
// - docs/superpowers/ — artefatos de planejamento vivem em _tasks/, não em docs/
|
||||
// - .eslintcache* .fakebin-* dist/ .build/ .artifacts/ logs/ — caches e outputs gerados
|
||||
//
|
||||
// Todos os prefixos são ancorados na raiz (startsWith sobre paths do `git ls-files`):
|
||||
// paths aninhados legítimos como `src/lib/logs/` NÃO são atingidos.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const FORBIDDEN_PREFIXES = ["node_modules/", ".next/", "coverage/"];
|
||||
const FORBIDDEN_PREFIXES = [
|
||||
"node_modules/",
|
||||
".next/",
|
||||
"coverage/",
|
||||
// "_" na raiz é GENÉRICO (regra abaixo em checkTrackedArtifacts): _tasks/, _references/,
|
||||
// _mono_repo/, _ideia/, _cache/ e qualquer _<novo>/ futuro — dirs privados, alguns com
|
||||
// repo git próprio (_tasks). Nunca rastrear nada dentro deles (Hard Rule #23).
|
||||
".claude/worktrees/",
|
||||
"docs/superpowers/",
|
||||
".eslintcache", // matches .eslintcache, .eslintcache-complexity, .eslintcache-probe, …
|
||||
".fakebin-", // test executable shim dirs (.fakebin-<pid>/)
|
||||
"dist/",
|
||||
".build/",
|
||||
".artifacts/",
|
||||
"logs/",
|
||||
];
|
||||
const FORBIDDEN_EXACT = new Set([
|
||||
"quality-metrics.json", // legacy root location (still forbidden if a stale run writes it)
|
||||
"config/quality/quality-metrics.json", // current generated location (collect-metrics.mjs)
|
||||
"_tasks", // separate git repo — a tracked blob/symlink here wiped the real dir twice (HR#23)
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -36,6 +61,13 @@ export function checkTrackedArtifacts(trackedFiles, trackedSymlinks = []) {
|
||||
violations.push(`forbidden tracked artifact: ${file}`);
|
||||
continue;
|
||||
}
|
||||
// Regra genérica: NENHUM caminho de raiz prefixado com "_" pode ser rastreado
|
||||
// (dir ou arquivo). Cobre _tasks, _references, _mono_repo e qualquer _<novo> futuro;
|
||||
// paths aninhados legítimos (src/lib/_x) não são atingidos.
|
||||
if (file.startsWith("_")) {
|
||||
violations.push(`forbidden tracked artifact (root underscore path): ${file}`);
|
||||
continue;
|
||||
}
|
||||
for (const prefix of FORBIDDEN_PREFIXES) {
|
||||
if (file.startsWith(prefix)) {
|
||||
violations.push(`forbidden tracked artifact (${prefix}*): ${file}`);
|
||||
|
||||
322
scripts/check/check-ts7-diagnostics-ratchet.mjs
Normal file
322
scripts/check/check-ts7-diagnostics-ratchet.mjs
Normal file
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env node
|
||||
// Blocks TypeScript 7 diagnostic regressions without requiring the existing
|
||||
// migration backlog to be clean. The PR base and checked-out head are compiled
|
||||
// with the same compiler and tsconfig, then compared as duplicate-preserving
|
||||
// multisets of: relative file | TS code | normalized message.
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const DEFAULT_TSCONFIG = "tsconfig.typecheck-core.json";
|
||||
const DEFAULT_COMPILER_VERSION = "7.0.2";
|
||||
const DIAGNOSTIC_START = /^(.+?)\((\d+),(\d+)\): error (TS\d+):\s*(.*)$/;
|
||||
const GLOBAL_DIAGNOSTIC_START = /^error (TS\d+):\s*(.*)$/;
|
||||
|
||||
function normalizeSlashes(value) {
|
||||
return String(value).replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function stripRoot(value, root) {
|
||||
const normalizedValue = normalizeSlashes(value);
|
||||
const normalizedRoot = normalizeSlashes(path.resolve(root)).replace(/\/$/, "");
|
||||
return normalizedValue === normalizedRoot
|
||||
? "."
|
||||
: normalizedValue.startsWith(`${normalizedRoot}/`)
|
||||
? normalizedValue.slice(normalizedRoot.length + 1)
|
||||
: normalizedValue;
|
||||
}
|
||||
|
||||
export function normalizeDiagnosticMessage(message, root = ROOT) {
|
||||
const normalizedRoot = normalizeSlashes(path.resolve(root)).replace(/\/$/, "");
|
||||
return normalizeSlashes(message)
|
||||
.replaceAll(normalizedRoot, "<repo>")
|
||||
.replace(/((?:[A-Za-z]:)?[^()\s]+\.(?:[cm]?[jt]sx?|json))\(\d+,\d+\)/gi, "$1")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Parse complete `tsc --pretty false` diagnostic blocks. */
|
||||
export function parseTscDiagnostics(raw, { root = ROOT } = {}) {
|
||||
const diagnostics = [];
|
||||
let current = null;
|
||||
|
||||
const flush = () => {
|
||||
if (!current) return;
|
||||
const message = normalizeDiagnosticMessage(current.messageLines.join("\n"), root);
|
||||
diagnostics.push({
|
||||
file: current.file,
|
||||
code: current.code,
|
||||
message,
|
||||
key: `${current.file}\u0000${current.code}\u0000${message}`,
|
||||
});
|
||||
current = null;
|
||||
};
|
||||
|
||||
for (const line of String(raw).split(/\r?\n/)) {
|
||||
const located = DIAGNOSTIC_START.exec(line);
|
||||
if (located) {
|
||||
flush();
|
||||
current = {
|
||||
file: stripRoot(located[1], root),
|
||||
code: located[4],
|
||||
messageLines: [located[5]],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const global = GLOBAL_DIAGNOSTIC_START.exec(line);
|
||||
if (global) {
|
||||
flush();
|
||||
current = { file: "<global>", code: global[1], messageLines: [global[2]] };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current && /^\s/.test(line) && line.trim()) current.messageLines.push(line);
|
||||
}
|
||||
flush();
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
export function toDiagnosticMultiset(diagnostics) {
|
||||
const counts = new Map();
|
||||
for (const diagnostic of diagnostics) {
|
||||
const entry = counts.get(diagnostic.key) ?? { ...diagnostic, count: 0 };
|
||||
entry.count += 1;
|
||||
counts.set(diagnostic.key, entry);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function diffDiagnosticMultisets(baseDiagnostics, headDiagnostics) {
|
||||
const base = toDiagnosticMultiset(baseDiagnostics);
|
||||
const head = toDiagnosticMultiset(headDiagnostics);
|
||||
const added = [];
|
||||
const removed = [];
|
||||
|
||||
for (const [key, entry] of head) {
|
||||
const baseCount = base.get(key)?.count ?? 0;
|
||||
if (entry.count > baseCount) {
|
||||
added.push({ ...entry, baseCount, headCount: entry.count, delta: entry.count - baseCount });
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of base) {
|
||||
const headCount = head.get(key)?.count ?? 0;
|
||||
if (entry.count > headCount) {
|
||||
removed.push({ ...entry, baseCount: entry.count, headCount, delta: entry.count - headCount });
|
||||
}
|
||||
}
|
||||
|
||||
const order = (a, b) => a.key.localeCompare(b.key);
|
||||
return { added: added.sort(order), removed: removed.sort(order) };
|
||||
}
|
||||
|
||||
export function hasParserPrerequisite(diagnostics) {
|
||||
return diagnostics.some((diagnostic) => diagnostic.code === "TS1005");
|
||||
}
|
||||
|
||||
function argument(name, fallback = "") {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return spawnSync(command, args, {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveCommit(ref) {
|
||||
const result = run("git", ["rev-parse", "--verify", `${ref}^{commit}`]);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`cannot resolve base ref ${ref}: ${result.stderr.trim()}`);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function sameLockfile(baseRoot) {
|
||||
const head = path.join(ROOT, "package-lock.json");
|
||||
const base = path.join(baseRoot, "package-lock.json");
|
||||
return (
|
||||
fs.existsSync(head) &&
|
||||
fs.existsSync(base) &&
|
||||
fs.readFileSync(head).equals(fs.readFileSync(base))
|
||||
);
|
||||
}
|
||||
|
||||
function linkDependencies(baseRoot) {
|
||||
const source = path.join(ROOT, "node_modules");
|
||||
const target = path.join(baseRoot, "node_modules");
|
||||
if (!fs.existsSync(source)) throw new Error("node_modules is missing; run npm ci first");
|
||||
|
||||
fs.mkdirSync(target);
|
||||
for (const entry of fs.readdirSync(source)) {
|
||||
if (entry === "@omniroute") continue;
|
||||
fs.symlinkSync(path.join(source, entry), path.join(target, entry), "junction");
|
||||
}
|
||||
|
||||
const scope = path.join(target, "@omniroute");
|
||||
fs.mkdirSync(scope);
|
||||
fs.symlinkSync(path.join(baseRoot, "open-sse"), path.join(scope, "open-sse"), "junction");
|
||||
fs.symlinkSync(
|
||||
path.join(baseRoot, "packages", "browser-pool"),
|
||||
path.join(scope, "browser-pool"),
|
||||
"junction"
|
||||
);
|
||||
}
|
||||
|
||||
function installBaseDependencies(baseRoot) {
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const result = run(
|
||||
npm,
|
||||
["ci", "--ignore-scripts", "--prefer-offline", "--no-audit", "--no-fund"],
|
||||
{ cwd: baseRoot, stdio: "inherit" }
|
||||
);
|
||||
if (result.status !== 0) throw new Error(`npm ci for the base worktree exited ${result.status}`);
|
||||
}
|
||||
|
||||
function runTypeScript(root, tsconfig, compilerVersion) {
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const result = run(
|
||||
npm,
|
||||
[
|
||||
"exec",
|
||||
"--yes",
|
||||
`--package=typescript@${compilerVersion}`,
|
||||
"--",
|
||||
"tsc",
|
||||
"--pretty",
|
||||
"false",
|
||||
"--noEmit",
|
||||
"-p",
|
||||
tsconfig,
|
||||
],
|
||||
{ cwd: root }
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
||||
const diagnostics = parseTscDiagnostics(output, { root });
|
||||
if (result.status !== 0 && diagnostics.length === 0) {
|
||||
throw new Error(
|
||||
`TypeScript exited ${result.status} without a parseable diagnostic:\n${output}`
|
||||
);
|
||||
}
|
||||
return { diagnostics, status: result.status ?? 0 };
|
||||
}
|
||||
|
||||
function formatEntry(entry) {
|
||||
return `${entry.file} ${entry.code}: ${entry.message} (${entry.baseCount} -> ${entry.headCount})`;
|
||||
}
|
||||
|
||||
function appendSummary({ baseRef, baseCount, headCount, added, removed, skipped }) {
|
||||
const summary = process.env.GITHUB_STEP_SUMMARY;
|
||||
if (!summary) return;
|
||||
const lines = [
|
||||
"## TypeScript 7 zero-new-diagnostics ratchet",
|
||||
"",
|
||||
`- Base: \`${baseRef}\` (${baseCount} diagnostics)`,
|
||||
`- Head: ${headCount} diagnostics`,
|
||||
`- Added: ${added.reduce((sum, entry) => sum + entry.delta, 0)}`,
|
||||
`- Removed: ${removed.reduce((sum, entry) => sum + entry.delta, 0)}`,
|
||||
];
|
||||
if (skipped) lines.push("- Status: parser prerequisite unresolved; comparison is advisory");
|
||||
if (added.length) {
|
||||
lines.push("", "### Added diagnostics", "", ...added.map((entry) => `- ${formatEntry(entry)}`));
|
||||
}
|
||||
fs.appendFileSync(summary, `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const baseRef = argument("--base-ref", process.env.TS7_BASE_REF ?? "");
|
||||
const tsconfig = argument("--tsconfig", DEFAULT_TSCONFIG);
|
||||
const compilerVersion = argument("--compiler-version", DEFAULT_COMPILER_VERSION);
|
||||
if (!baseRef) {
|
||||
console.log("[ts7-ratchet] SKIP — --base-ref is required outside a pull request");
|
||||
return 0;
|
||||
}
|
||||
if (!fs.existsSync(path.join(ROOT, tsconfig))) {
|
||||
throw new Error(`tsconfig not found: ${tsconfig}`);
|
||||
}
|
||||
|
||||
const baseCommit = resolveCommit(baseRef);
|
||||
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ts7-ratchet-"));
|
||||
const baseRoot = path.join(temporaryRoot, "base");
|
||||
let worktreeAdded = false;
|
||||
|
||||
try {
|
||||
const add = run("git", ["worktree", "add", "--detach", baseRoot, baseCommit]);
|
||||
if (add.status !== 0) throw new Error(`cannot create base worktree: ${add.stderr.trim()}`);
|
||||
worktreeAdded = true;
|
||||
|
||||
if (sameLockfile(baseRoot)) linkDependencies(baseRoot);
|
||||
else installBaseDependencies(baseRoot);
|
||||
|
||||
console.log(
|
||||
`[ts7-ratchet] TypeScript ${compilerVersion}; base=${baseCommit}; config=${tsconfig}`
|
||||
);
|
||||
const base = runTypeScript(baseRoot, tsconfig, compilerVersion);
|
||||
const head = runTypeScript(ROOT, tsconfig, compilerVersion);
|
||||
const { added, removed } = diffDiagnosticMultisets(base.diagnostics, head.diagnostics);
|
||||
const parserBlocked = hasParserPrerequisite(base.diagnostics);
|
||||
|
||||
console.log(`ts7DiagnosticsBase=${base.diagnostics.length}`);
|
||||
console.log(`ts7DiagnosticsHead=${head.diagnostics.length}`);
|
||||
console.log(`ts7DiagnosticsAdded=${added.reduce((sum, entry) => sum + entry.delta, 0)}`);
|
||||
console.log(`ts7DiagnosticsRemoved=${removed.reduce((sum, entry) => sum + entry.delta, 0)}`);
|
||||
|
||||
appendSummary({
|
||||
baseRef: baseCommit,
|
||||
baseCount: base.diagnostics.length,
|
||||
headCount: head.diagnostics.length,
|
||||
added,
|
||||
removed,
|
||||
skipped: parserBlocked,
|
||||
});
|
||||
|
||||
if (parserBlocked) {
|
||||
console.warn(
|
||||
"[ts7-ratchet] SKIP — the release base still has TS1005 parser diagnostics. " +
|
||||
"Resolve #10094 before making this comparison blocking; those errors are not accepted as baseline."
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (added.length) {
|
||||
console.error(
|
||||
`[ts7-ratchet] FAIL — the PR adds ${added.reduce((sum, entry) => sum + entry.delta, 0)} ` +
|
||||
`normalized TypeScript 7 diagnostic(s):\n${added.map((entry) => ` ✗ ${formatEntry(entry)}`).join("\n")}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ts7-ratchet] OK — no new normalized diagnostics; ` +
|
||||
`${removed.reduce((sum, entry) => sum + entry.delta, 0)} removed.`
|
||||
);
|
||||
return 0;
|
||||
} finally {
|
||||
if (worktreeAdded) {
|
||||
const remove = run("git", ["worktree", "remove", "--force", baseRoot]);
|
||||
if (remove.status !== 0) {
|
||||
console.warn(`[ts7-ratchet] WARN — temporary worktree cleanup: ${remove.stderr.trim()}`);
|
||||
}
|
||||
run("git", ["worktree", "prune"]);
|
||||
}
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
try {
|
||||
process.exitCode = main();
|
||||
} catch (error) {
|
||||
console.error(`[ts7-ratchet] FAIL — ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exitCode = 2;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
FREE_PROVIDERS,
|
||||
NOAUTH_PROVIDERS,
|
||||
OAUTH_PROVIDERS,
|
||||
WEB_COOKIE_PROVIDERS,
|
||||
APIKEY_PROVIDERS,
|
||||
@@ -136,6 +137,7 @@ function buildHeader(total: number): string {
|
||||
"## Categories",
|
||||
"",
|
||||
"- **Free** — free tier with API key (configured via dashboard)",
|
||||
"- **No-auth** — public endpoints that require no key or sign-in at all",
|
||||
"- **OAuth** — sign-in flow handled by OmniRoute, no API key needed",
|
||||
"- **Web cookie** — wraps the provider's web app via cookie auth",
|
||||
"- **API key** — paid provider configured via API key (free credits may apply)",
|
||||
@@ -159,8 +161,19 @@ function buildHeader(total: number): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function countExecutorImpls(): number {
|
||||
const dir = path.join(ROOT, "open-sse", "executors");
|
||||
const nonImpl = new Set(["index.ts", "index.mts", "types.ts", "base.ts", "constants.ts"]);
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter(
|
||||
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.startsWith("__") && !nonImpl.has(f)
|
||||
).length;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const free = asRecords(FREE_PROVIDERS);
|
||||
const noauth = asRecords(NOAUTH_PROVIDERS as Record<string, ProviderRecord>);
|
||||
const oauth = asRecords(OAUTH_PROVIDERS);
|
||||
const webCookie = asRecords(WEB_COOKIE_PROVIDERS);
|
||||
const apiKey = asRecords(APIKEY_PROVIDERS);
|
||||
@@ -173,6 +186,7 @@ function main() {
|
||||
|
||||
const allIds = new Set<string>([
|
||||
...free.map((p) => p.id),
|
||||
...noauth.map((p) => p.id),
|
||||
...oauth.map((p) => p.id),
|
||||
...webCookie.map((p) => p.id),
|
||||
...apiKey.map((p) => p.id),
|
||||
@@ -186,6 +200,7 @@ function main() {
|
||||
|
||||
const sections = [
|
||||
buildSection("Free Tier (OAuth-first or no-key)", free, "Free"),
|
||||
buildSection("No-auth Providers (no key required)", noauth, "No-auth"),
|
||||
buildSection("OAuth Providers", oauth, "OAuth"),
|
||||
buildSection("Web Cookie Providers", webCookie, "Web cookie"),
|
||||
buildSection("API Key Providers (paid / paid-with-free-credits)", apiKey, "API key"),
|
||||
@@ -202,7 +217,7 @@ function main() {
|
||||
"",
|
||||
"- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)",
|
||||
"- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)",
|
||||
"- Executors: [`open-sse/executors/`](../../open-sse/executors/) (31 files)",
|
||||
`- Executors: [\`open-sse/executors/\`](../../open-sse/executors/) (${countExecutorImpls()} implementations)`,
|
||||
"- Translators: [`open-sse/translator/`](../../open-sse/translator/)",
|
||||
"",
|
||||
"## See Also",
|
||||
@@ -218,9 +233,10 @@ function main() {
|
||||
console.log(`✓ Wrote ${OUT_FILE}`);
|
||||
console.log(` Providers: ${allIds.size} unique IDs`);
|
||||
console.log(
|
||||
` Sections: free=${free.length}, oauth=${oauth.length}, web=${webCookie.length}, ` +
|
||||
`apikey=${apiKey.length}, local=${local.length}, search=${search.length}, ` +
|
||||
`audio=${audio.length}, proxy=${upstreamProxy.length}, cloud=${cloudAgent.length}, system=${system.length}`
|
||||
` Sections: free=${free.length}, noauth=${noauth.length}, oauth=${oauth.length}, ` +
|
||||
`web=${webCookie.length}, apikey=${apiKey.length}, local=${local.length}, ` +
|
||||
`search=${search.length}, audio=${audio.length}, proxy=${upstreamProxy.length}, ` +
|
||||
`cloud=${cloudAgent.length}, system=${system.length}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_SRC="$(dirname "$SCRIPT_DIR")/obsidian-plugin"
|
||||
DESKTOP_VAULT="${1:-$HOME/Documents/Vault/Omniroute-Test}"
|
||||
MOBILE_VAULT="${2:-$HOME/Documents/Vault/Test}"
|
||||
|
||||
echo "Building plugin..."
|
||||
cd "$PLUGIN_SRC"
|
||||
npm run build 2>&1 | tail -3
|
||||
|
||||
echo "Installing to desktop vault: $DESKTOP_VAULT"
|
||||
mkdir -p "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync"
|
||||
cp "$PLUGIN_SRC/dist/main.js" "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync/"
|
||||
cp "$PLUGIN_SRC/manifest.json" "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync/"
|
||||
cp "$PLUGIN_SRC/styles.css" "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync/"
|
||||
echo " ✓ Desktop plugin installed"
|
||||
|
||||
if [ -d "$MOBILE_VAULT" ]; then
|
||||
echo "Installing to mobile vault: $MOBILE_VAULT"
|
||||
mkdir -p "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync"
|
||||
cp "$PLUGIN_SRC/dist/main.js" "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync/"
|
||||
cp "$PLUGIN_SRC/manifest.json" "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync/"
|
||||
cp "$PLUGIN_SRC/styles.css" "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync/"
|
||||
echo " ✓ Mobile plugin installed"
|
||||
fi
|
||||
|
||||
echo "Done! Restart Obsidian on both devices to load the plugin."
|
||||
@@ -1,12 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.resolve(process.env.USERPROFILE, '.omniroute', 'storage.sqlite');
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db.prepare(`SELECT id, provider, name, auth_type, is_active, last_error, test_status, updated_at FROM provider_connections ORDER BY updated_at DESC LIMIT 200`).all();
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
db.close();
|
||||
} catch (err) {
|
||||
console.error('ERROR', err && err.message);
|
||||
process.exit(2);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.resolve(process.env.USERPROFILE, '.omniroute', 'storage.sqlite');
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db.prepare(`SELECT id, provider, name, auth_type, is_active, api_key IS NOT NULL as has_api_key, access_token IS NOT NULL as has_access_token, refresh_token IS NOT NULL as has_refresh, last_error, test_status, provider_specific_data, updated_at FROM provider_connections WHERE provider LIKE '%anthropic%' OR provider='anthropic' OR provider LIKE '%claude%' OR provider='claude'`).all();
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
db.close();
|
||||
} catch (err) {
|
||||
console.error('ERROR', err && err.message);
|
||||
process.exit(2);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
const Database = require("better-sqlite3");
|
||||
const path = require("path");
|
||||
const dbPath = path.resolve(process.env.USERPROFILE, ".omniroute", "storage.sqlite");
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, provider, name, auth_type, is_active, api_key IS NOT NULL as has_api_key, access_token IS NOT NULL as has_access_token, refresh_token IS NOT NULL as has_refresh, last_error, test_status, provider_specific_data, updated_at FROM provider_connections WHERE provider LIKE '%anthropic%' OR provider='anthropic' OR provider LIKE '%claude%' OR provider='claude'`
|
||||
)
|
||||
.all();
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
db.close();
|
||||
} catch (err) {
|
||||
console.error("ERROR", err && err.message);
|
||||
process.exit(2);
|
||||
}
|
||||
Reference in New Issue
Block a user