mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
refactor(scripts): organize into build/dev/check/docs/i18n/ad-hoc subfolders
Reorganizes the 29 active scripts under scripts/ into purpose-driven subfolders: - scripts/build/ (11) — Build, install, publish, runtime env - scripts/dev/ (13) — Dev servers, test runners, healthchecks - scripts/check/ (10) — Lint/validation/coverage checks - scripts/docs/ (2) — Docs index and provider reference generation - scripts/i18n/ (+3) — Adds Python translation utilities (check/validate/autotranslate) - scripts/ad-hoc/ (4) — One-shot maintenance utilities Updates all references in package.json, electron/package.json, .husky/pre-commit, .github/workflows/ci.yml, Dockerfile, src/, tests/, scripts/ internal cross-imports, playwright.config.ts, and English docs (CODEBASE_DOCUMENTATION, ENVIRONMENT, FEATURES, RELEASE_CHECKLIST, COVERAGE_PLAN, ELECTRON_GUIDE, I18N, GEMINI). Also patches scripts/build/pack-artifact-policy.ts so the npm pack allowlist mirrors the new layout. Validates with: - npm run lint (exit 0 — pre-existing minified-bundle errors only) - npm run typecheck:core (exit 0) - npm run check:docs-all (exit 0) - unit tests for moved scripts (57 tests pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
192
scripts/docs/gen-provider-reference.ts
Normal file
192
scripts/docs/gen-provider-reference.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates docs/PROVIDER_REFERENCE.md from src/shared/constants/providers.ts.
|
||||
// Run: node --import tsx/esm scripts/docs/gen-provider-reference.ts
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
FREE_PROVIDERS,
|
||||
OAUTH_PROVIDERS,
|
||||
WEB_COOKIE_PROVIDERS,
|
||||
APIKEY_PROVIDERS,
|
||||
LOCAL_PROVIDERS,
|
||||
SEARCH_PROVIDERS,
|
||||
AUDIO_ONLY_PROVIDERS,
|
||||
UPSTREAM_PROXY_PROVIDERS,
|
||||
CLOUD_AGENT_PROVIDERS,
|
||||
SYSTEM_PROVIDERS,
|
||||
IMAGE_ONLY_PROVIDER_IDS,
|
||||
AGGREGATOR_PROVIDER_IDS,
|
||||
ENTERPRISE_CLOUD_PROVIDER_IDS,
|
||||
VIDEO_PROVIDER_IDS,
|
||||
EMBEDDING_RERANK_PROVIDER_IDS,
|
||||
SELF_HOSTED_CHAT_PROVIDER_IDS,
|
||||
} from "../../src/shared/constants/providers.ts";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const OUT_FILE = path.join(ROOT, "docs", "PROVIDER_REFERENCE.md");
|
||||
|
||||
type ProviderRecord = {
|
||||
id: string;
|
||||
alias?: string | undefined;
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
textIcon?: string;
|
||||
website?: string;
|
||||
authHint?: string;
|
||||
freeNote?: string;
|
||||
hasFree?: boolean;
|
||||
deprecated?: boolean;
|
||||
deprecationReason?: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
function asRecords(map: Record<string, ProviderRecord>): ProviderRecord[] {
|
||||
return Object.values(map).map((p) => ({ ...p }));
|
||||
}
|
||||
|
||||
function escapeCell(value: string | undefined): string {
|
||||
if (!value) return "—";
|
||||
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
||||
}
|
||||
|
||||
function row(p: ProviderRecord, category: string): string {
|
||||
const alias = p.alias ? `\`${p.alias}\`` : "—";
|
||||
const hint = p.deprecated
|
||||
? `⚠️ **DEPRECATED.** ${escapeCell(p.deprecationReason)}`
|
||||
: escapeCell(p.authHint || p.freeNote);
|
||||
const link = p.website ? `[link](${p.website})` : "—";
|
||||
return `| \`${p.id}\` | ${alias} | ${escapeCell(p.name)} | ${category} | ${link} | ${hint} |`;
|
||||
}
|
||||
|
||||
function categoryTags(id: string): string[] {
|
||||
const tags: string[] = [];
|
||||
if (IMAGE_ONLY_PROVIDER_IDS.has(id)) tags.push("image");
|
||||
if (VIDEO_PROVIDER_IDS.has(id)) tags.push("video");
|
||||
if (AGGREGATOR_PROVIDER_IDS.has(id)) tags.push("aggregator");
|
||||
if (ENTERPRISE_CLOUD_PROVIDER_IDS.has(id)) tags.push("enterprise");
|
||||
if (EMBEDDING_RERANK_PROVIDER_IDS.has(id)) tags.push("embed/rerank");
|
||||
if (SELF_HOSTED_CHAT_PROVIDER_IDS.has(id)) tags.push("self-hosted");
|
||||
return tags;
|
||||
}
|
||||
|
||||
function sortById(rows: ProviderRecord[]): ProviderRecord[] {
|
||||
return [...rows].sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
function buildSection(title: string, rows: ProviderRecord[], category: string): string {
|
||||
if (rows.length === 0) return "";
|
||||
const lines: string[] = [];
|
||||
lines.push(`## ${title} (${rows.length})\n`);
|
||||
lines.push("| ID | Alias | Name | Tags | Website | Notes |");
|
||||
lines.push("|----|-------|------|------|---------|-------|");
|
||||
for (const p of sortById(rows)) {
|
||||
const tags = [category, ...categoryTags(p.id)].join(", ");
|
||||
lines.push(row(p, tags));
|
||||
}
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildHeader(total: number): string {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
return [
|
||||
"# Provider Reference",
|
||||
"",
|
||||
`> **Auto-generated** from \`src/shared/constants/providers.ts\` — do not edit by hand.`,
|
||||
`> Regenerate with: \`npm run gen:provider-reference\``,
|
||||
`> **Last generated:** ${date}`,
|
||||
"",
|
||||
`Total providers: **${total}**. See category breakdown below.`,
|
||||
"",
|
||||
"## Categories",
|
||||
"",
|
||||
"- **Free** — free tier with API key (configured via dashboard)",
|
||||
"- **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)",
|
||||
"- **Local** — runs on the user's machine (Ollama, LM Studio, vLLM, etc.)",
|
||||
"- **Search** — web search providers",
|
||||
"- **Audio** — audio-only providers (TTS/STT)",
|
||||
"- **Upstream proxy** — providers that proxy to other providers",
|
||||
"- **Cloud agent** — long-running coding agents (Codex Cloud, Devin, Jules)",
|
||||
"- **System** — OmniRoute-internal providers (loopback, etc.)",
|
||||
"",
|
||||
"Additional tags: `image`, `video`, `aggregator`, `enterprise`, `embed/rerank`, `self-hosted`.",
|
||||
"",
|
||||
"Use the dashboard at `/dashboard/providers` to enable, configure, and test each provider.",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function main() {
|
||||
const free = asRecords(FREE_PROVIDERS);
|
||||
const oauth = asRecords(OAUTH_PROVIDERS);
|
||||
const webCookie = asRecords(WEB_COOKIE_PROVIDERS);
|
||||
const apiKey = asRecords(APIKEY_PROVIDERS);
|
||||
const local = asRecords(LOCAL_PROVIDERS);
|
||||
const search = asRecords(SEARCH_PROVIDERS);
|
||||
const audio = asRecords(AUDIO_ONLY_PROVIDERS);
|
||||
const upstreamProxy = asRecords(UPSTREAM_PROXY_PROVIDERS);
|
||||
const cloudAgent = asRecords(CLOUD_AGENT_PROVIDERS);
|
||||
const system = asRecords(SYSTEM_PROVIDERS);
|
||||
|
||||
const allIds = new Set<string>([
|
||||
...free.map((p) => p.id),
|
||||
...oauth.map((p) => p.id),
|
||||
...webCookie.map((p) => p.id),
|
||||
...apiKey.map((p) => p.id),
|
||||
...local.map((p) => p.id),
|
||||
...search.map((p) => p.id),
|
||||
...audio.map((p) => p.id),
|
||||
...upstreamProxy.map((p) => p.id),
|
||||
...cloudAgent.map((p) => p.id),
|
||||
...system.map((p) => p.id),
|
||||
]);
|
||||
|
||||
const sections = [
|
||||
buildSection("Free Tier (OAuth-first or no-key)", free, "Free"),
|
||||
buildSection("OAuth Providers", oauth, "OAuth"),
|
||||
buildSection("Web Cookie Providers", webCookie, "Web cookie"),
|
||||
buildSection("API Key Providers (paid / paid-with-free-credits)", apiKey, "API key"),
|
||||
buildSection("Local Providers", local, "Local"),
|
||||
buildSection("Search Providers", search, "Search"),
|
||||
buildSection("Audio-only Providers", audio, "Audio"),
|
||||
buildSection("Upstream Proxy Providers", upstreamProxy, "Upstream proxy"),
|
||||
buildSection("Cloud Agent Providers", cloudAgent, "Cloud agent"),
|
||||
buildSection("System Providers", system, "System"),
|
||||
];
|
||||
|
||||
const footer = [
|
||||
"## Sources of truth",
|
||||
"",
|
||||
"- 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)",
|
||||
"- Translators: [`open-sse/translator/`](../open-sse/translator/)",
|
||||
"",
|
||||
"## See Also",
|
||||
"",
|
||||
"- [FREE_TIERS.md](./FREE_TIERS.md) — curated free-tier guide",
|
||||
"- [USER_GUIDE.md](./USER_GUIDE.md) — provider setup walkthrough",
|
||||
"- [ARCHITECTURE.md](./ARCHITECTURE.md) — overall architecture",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const content = buildHeader(allIds.size) + sections.join("\n") + "\n" + footer;
|
||||
fs.writeFileSync(OUT_FILE, content);
|
||||
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}`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
276
scripts/docs/generate-docs-index.mjs
Normal file
276
scripts/docs/generate-docs-index.mjs
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build-time script: scans docs/*.md and generates
|
||||
* src/app/docs/lib/docs-auto-generated.ts with static navigation + search data.
|
||||
*
|
||||
* This file is imported by both client and server components — NO fs/path imports.
|
||||
* Run via: node scripts/docs/generate-docs-index.mjs
|
||||
* Automatically runs as prebuild step.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import matter from "gray-matter";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const DOCS_DIR = path.join(ROOT, "docs");
|
||||
const OUT_FILE = path.join(ROOT, "src", "app", "docs", "lib", "docs-auto-generated.ts");
|
||||
|
||||
const SECTION_CATEGORIES = {
|
||||
"Getting Started": [
|
||||
"SETUP_GUIDE",
|
||||
"USER_GUIDE",
|
||||
"CLI_TOOLS",
|
||||
"ARCHITECTURE",
|
||||
"QUICK_START",
|
||||
"GETTING_STARTED",
|
||||
],
|
||||
Features: [
|
||||
"FEATURES",
|
||||
"AUTO_COMBO",
|
||||
"COMPRESSION_GUIDE",
|
||||
"RTK_COMPRESSION",
|
||||
"COMPRESSION_ENGINES",
|
||||
"COMPRESSION_RULES_FORMAT",
|
||||
"COMPRESSION_LANGUAGE_PACKS",
|
||||
"FREE_TIERS",
|
||||
],
|
||||
"API & Protocols": ["API_REFERENCE", "MCP_SERVER", "A2A_SERVER"],
|
||||
Deployment: [
|
||||
"DOCKER_GUIDE",
|
||||
"VM_DEPLOYMENT_GUIDE",
|
||||
"FLY_IO_DEPLOYMENT_GUIDE",
|
||||
"TERMUX_GUIDE",
|
||||
"PWA_GUIDE",
|
||||
],
|
||||
Operations: ["PROXY_GUIDE", "RESILIENCE_GUIDE", "ENVIRONMENT", "TROUBLESHOOTING"],
|
||||
Development: [
|
||||
"CODEBASE_DOCUMENTATION",
|
||||
"COVERAGE_PLAN",
|
||||
"I18N",
|
||||
"RELEASE_CHECKLIST",
|
||||
"UNINSTALL",
|
||||
"CONTRIBUTING",
|
||||
"CHANGELOG",
|
||||
"CODE_OF_CONDUCT",
|
||||
],
|
||||
};
|
||||
|
||||
const SECTION_ORDER = {
|
||||
"Getting Started": 1,
|
||||
Features: 2,
|
||||
"API & Protocols": 3,
|
||||
Deployment: 4,
|
||||
Operations: 5,
|
||||
Development: 6,
|
||||
};
|
||||
|
||||
function categorizeFile(fileName) {
|
||||
const stem = fileName.replace(/\.md$/i, "").toUpperCase().replace(/-/g, "_");
|
||||
for (const [section, patterns] of Object.entries(SECTION_CATEGORIES)) {
|
||||
if (patterns.some((p) => stem === p)) {
|
||||
return section;
|
||||
}
|
||||
}
|
||||
return "Other";
|
||||
}
|
||||
|
||||
function extractTitleFromContent(content) {
|
||||
const match = content.match(/^#\s+(.+)$/m);
|
||||
if (match) {
|
||||
return match[1]
|
||||
.replace(/^📖\s*/, "")
|
||||
.replace(/^🌐\s*/, "")
|
||||
.replace(/\s*—\s*OmniRoute\s*$/i, "")
|
||||
.replace(/\s*—\s*OmniRoute Docs\s*$/i, "")
|
||||
.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractHeadings(content) {
|
||||
const headings = [];
|
||||
const regex = /^(#{2,4})\s+(.+)$/gm;
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
headings.push(match[2].replace(/\*\*/g, "").replace(/\*/g, "").replace(/`/g, "").trim());
|
||||
}
|
||||
return headings.slice(0, 10);
|
||||
}
|
||||
|
||||
function extractContentPreview(content) {
|
||||
const stripped = content
|
||||
.replace(/^---[\s\S]*?---/m, "")
|
||||
.replace(/^#{1,6}\s+.+$/gm, "")
|
||||
.replace(/```[\s\S]*?```/g, "")
|
||||
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/[*_`~>#|]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return stripped.slice(0, 300);
|
||||
}
|
||||
|
||||
// ---------- Main ----------
|
||||
|
||||
if (!fs.existsSync(DOCS_DIR)) {
|
||||
if (fs.existsSync(OUT_FILE)) {
|
||||
console.warn(
|
||||
`[generate-docs-index] ${DOCS_DIR} not found; keeping existing generated docs index.`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
OUT_FILE,
|
||||
`// AUTO-GENERATED by scripts/docs/generate-docs-index.mjs — DO NOT EDIT MANUALLY
|
||||
// Regenerate with: node scripts/docs/generate-docs-index.mjs
|
||||
|
||||
export interface AutoGenDocItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface AutoGenNavSection {
|
||||
title: string;
|
||||
items: AutoGenDocItem[];
|
||||
}
|
||||
|
||||
export interface AutoGenSearchItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
fileName: string;
|
||||
section: string;
|
||||
content: string;
|
||||
headings: string[];
|
||||
}
|
||||
|
||||
export const autoNavSections: AutoGenNavSection[] = [];
|
||||
|
||||
export const autoSearchIndex: AutoGenSearchItem[] = [];
|
||||
|
||||
export const autoAllSlugs: string[] = [];
|
||||
`,
|
||||
"utf8"
|
||||
);
|
||||
console.warn(`[generate-docs-index] ${DOCS_DIR} not found; generated empty docs index.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(DOCS_DIR).filter((f) => f.endsWith(".md") || f.endsWith(".mdx"));
|
||||
|
||||
const docs = [];
|
||||
for (const fileName of files) {
|
||||
const filePath = path.join(DOCS_DIR, fileName);
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
const { data: frontmatter, content } = matter(fileContent);
|
||||
|
||||
const slug =
|
||||
frontmatter.slug ||
|
||||
fileName
|
||||
.replace(/\.mdx?$/i, "")
|
||||
.toLowerCase()
|
||||
.replace(/_/g, "-");
|
||||
const title = frontmatter.title || extractTitleFromContent(content) || slug.replace(/-/g, " ");
|
||||
const section = frontmatter.section || categorizeFile(fileName);
|
||||
const order = frontmatter.order ?? 999;
|
||||
const headings = extractHeadings(content);
|
||||
const contentPreview = frontmatter.description || extractContentPreview(content);
|
||||
|
||||
docs.push({ slug, title, fileName, section, order, content: contentPreview, headings });
|
||||
}
|
||||
|
||||
docs.sort((a, b) => {
|
||||
const sectionA = SECTION_ORDER[a.section] ?? 99;
|
||||
const sectionB = SECTION_ORDER[b.section] ?? 99;
|
||||
if (sectionA !== sectionB) return sectionA - sectionB;
|
||||
return a.order - b.order;
|
||||
});
|
||||
|
||||
// Build navigation sections
|
||||
const sectionMap = new Map();
|
||||
for (const doc of docs) {
|
||||
const items = sectionMap.get(doc.section) || [];
|
||||
items.push(doc);
|
||||
sectionMap.set(doc.section, items);
|
||||
}
|
||||
const orderedSections = [...new Set([...Object.keys(SECTION_ORDER), ...sectionMap.keys()])];
|
||||
const navSections = orderedSections
|
||||
.filter((s) => sectionMap.has(s))
|
||||
.sort((a, b) => (SECTION_ORDER[a] ?? 99) - (SECTION_ORDER[b] ?? 99))
|
||||
.map((title) => ({
|
||||
title,
|
||||
items: sectionMap.get(title).map((doc) => ({
|
||||
slug: doc.slug,
|
||||
title: doc.title,
|
||||
fileName: doc.fileName,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Build search index
|
||||
const searchIndex = docs.map((doc) => ({
|
||||
slug: doc.slug,
|
||||
title: doc.title,
|
||||
fileName: doc.fileName,
|
||||
section: doc.section,
|
||||
content: doc.content,
|
||||
headings: doc.headings,
|
||||
}));
|
||||
|
||||
// Add api-explorer synthetic entry if api-reference exists
|
||||
if (searchIndex.some((item) => item.slug === "api-reference")) {
|
||||
if (!searchIndex.some((item) => item.slug === "api-explorer")) {
|
||||
searchIndex.push({
|
||||
slug: "api-explorer",
|
||||
title: "API Explorer",
|
||||
fileName: "API_REFERENCE.md",
|
||||
section: "API & Protocols",
|
||||
content: "interactive try it live api explorer endpoint test request response curl example",
|
||||
headings: ["Try It", "Endpoints"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Write output ----------
|
||||
|
||||
const output = `// AUTO-GENERATED by scripts/docs/generate-docs-index.mjs — DO NOT EDIT MANUALLY
|
||||
// Regenerate with: node scripts/docs/generate-docs-index.mjs
|
||||
|
||||
export interface AutoGenDocItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface AutoGenNavSection {
|
||||
title: string;
|
||||
items: AutoGenDocItem[];
|
||||
}
|
||||
|
||||
export interface AutoGenSearchItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
fileName: string;
|
||||
section: string;
|
||||
content: string;
|
||||
headings: string[];
|
||||
}
|
||||
|
||||
export const autoNavSections: AutoGenNavSection[] = ${JSON.stringify(navSections, null, 2)};
|
||||
|
||||
export const autoSearchIndex: AutoGenSearchItem[] = ${JSON.stringify(searchIndex, null, 2)};
|
||||
|
||||
export const autoAllSlugs: string[] = ${JSON.stringify(
|
||||
docs.map((d) => d.slug),
|
||||
null,
|
||||
2
|
||||
)};
|
||||
`;
|
||||
|
||||
fs.writeFileSync(OUT_FILE, output, "utf8");
|
||||
console.log(`✅ Generated ${OUT_FILE} with ${docs.length} docs, ${navSections.length} sections`);
|
||||
Reference in New Issue
Block a user