diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41d363a --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +# Keep pnpm linking the in-repo workspace packages (@gitpagedocs/tools, /mcp) +# even though their dependents declare plain semver ranges (^1.1.x) instead of +# the `workspace:*` protocol. Plain ranges are used so the PUBLISHED packages +# carry resolvable npm deps regardless of whether they are released with +# `pnpm publish` or `npm publish` (npm does not rewrite `workspace:*`). +link-workspace-packages=true +prefer-workspace-packages=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6d26ad..7eb484d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,9 @@ This is a pnpm + turbo monorepo: `frontend/` (Next.js viewer), `cli/` (the publi - `gitpagedocs config` — show the resolved gitpagedocs config - `gitpagedocs provider [id]` — list AI providers or show one - `gitpagedocs models [provider]` — list catalog models -- `gitpagedocs document[:repo|:file|:folder]` — generate documentation with AI +- `gitpagedocs ai` — interactive AI docs generator (writes pages in the gitpagedocs pattern) +- `gitpagedocs document[:repo|:file|:folder]` — generate documentation with AI in the gitpagedocs pattern +- `gitpagedocs password` — set a documentation access password (writes the public key to config.json) - `gitpagedocs deploy | pages` — configure GitHub Pages via Actions and push - `gitpagedocs doctor` — diagnose the environment - `gitpagedocs mcp start` — start the MCP server over stdio diff --git a/README.md b/README.md index 7ad0bab..091ade6 100644 --- a/README.md +++ b/README.md @@ -473,6 +473,17 @@ This mode provides: - optional `.gitpagedocsconfig` persistence for manual reuse - interactive fallback when directories are missing (fix/skip/abort) +**Generates in the gitpagedocs pattern.** The model is told what gitpagedocs is and +returns documentation split into multiple pages. `gitpagedocs ai` first scaffolds the base +`gitpagedocs/` structure, then writes each page to +`gitpagedocs/docs/versions///.md` (in every language) **and wires them +into that version's `config.json`** (`routes-md` + `menus-header-md`), so the AI pages show up +directly in the docs viewer menu. The added entries are tagged `aiGenerated` and are +idempotent — re-running `gitpagedocs ai` replaces them instead of duplicating. + +> Note: a later plain `gitpagedocs` run rebuilds the base config from the deterministic +> templates and drops the AI wiring — re-run `gitpagedocs ai` to restore it. + ### Manual config (`.gitpagedocsconfig`) You can run manually with a persisted config in repository root: @@ -495,6 +506,23 @@ You can run manually with a persisted config in repository root: For Ollama, use `baseUrl` instead of `apiKey`. +## Documentation password gate + +Protect the whole documentation site behind a password: + +```bash +gitpagedocs password +``` + +It prompts for a password (type + confirm), writes a non-reversible **public key** to +`site.docsAccess` in `gitpagedocs/config.json`, and prints a **private key** to copy. The +scheme is double-hash: `privateKey = SHA256(password)`, `publicKey = SHA256(privateKey)` — the +password itself is never stored. When `docsAccess.enabled` is set, the viewer blocks the entire +documentation behind a full-page gate; visitors unlock with the **password OR the private key** +(verified against the public key). The unlock is cached in `localStorage`, and a lock icon in +the menu clears the cache to re-block. Leave `docsAccess.enabled` false (the default) to keep +the docs open. + ## Configuration File Format Runtime supports three config file formats (in order of precedence): @@ -533,7 +561,9 @@ ISC. See [repository](https://github.com/Vidigal-code/git-page-docs) for details - `gitpagedocs config` — show the resolved gitpagedocs config - `gitpagedocs provider [id]` — list AI providers or show one - `gitpagedocs models [provider]` — list catalog models -- `gitpagedocs document[:repo|:file|:folder]` — generate documentation with AI +- `gitpagedocs ai` — interactive AI docs generator (writes pages in the gitpagedocs pattern) +- `gitpagedocs document[:repo|:file|:folder]` — generate documentation with AI in the gitpagedocs pattern +- `gitpagedocs password` — set a documentation access password (writes the public key to config.json) - `gitpagedocs deploy | pages` — configure GitHub Pages via Actions and push - `gitpagedocs doctor` — diagnose the environment - `gitpagedocs mcp start` — start the MCP server over stdio diff --git a/cli/README.md b/cli/README.md index a3f41c4..44db96d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -21,9 +21,10 @@ npx @gitpagedocs/cli | `gitpagedocs --push --owner --repo [--path ]` | Generate, configure Pages, create workflow, commit + push | | `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`: static site + .env + Dockerfile) | | `gitpagedocs -i` / `--interactive` | Interactive prompts | -| `gitpagedocs ai` | Interactive AI documentation generator (see below) | +| `gitpagedocs ai` | Interactive AI documentation generator — writes pages in the gitpagedocs pattern (see below) | | `gitpagedocs provider [id]` · `models [provider]` | List AI providers / catalog models | -| `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI | +| `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI in the gitpagedocs pattern | +| `gitpagedocs password` | Set a documentation access password (writes the public key to `config.json`, prints the private key) | | `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push | | `gitpagedocs docs` | Refresh managed regions of README/CONTRIBUTING/SECURITY | | `gitpagedocs doctor` · `version` · `update` | Diagnostics / version / update hint | @@ -134,9 +135,18 @@ npx gitpagedocs ai - asks provider (`openai`, `claude`, `gemini`, `ollama`) - asks API key or Ollama URL - asks paths to scan (supports one or many paths, including other repositories) -- generates markdown documentation in `pt`, `en`, `es` +- generates documentation in `pt`, `en`, `es` **in the gitpagedocs pattern** - optionally persists config in `.gitpagedocsconfig` -- optionally runs standard `gitpagedocs` scaffolding after AI generation + +### gitpagedocs pattern output + +The model is given a gitpagedocs-aware system prompt and returns documentation split into +pages (delimited by `=== PAGE: | ===`). The CLI scaffolds the base +`gitpagedocs/` structure first, then writes each page to +`gitpagedocs/docs/versions/<latest>/<lang>/<slug>.md` for every language and **wires it into +that version's `config.json`** (`routes-md` + `menus-header-md`) so the pages render in the +viewer menu. Entries are tagged `aiGenerated` and idempotent (re-running replaces them). A +later plain `gitpagedocs` rebuild drops the wiring — re-run `gitpagedocs ai` to restore. ### `.gitpagedocsconfig` (manual mode) diff --git a/cli/ai/application/docs-pattern.ts b/cli/ai/application/docs-pattern.ts new file mode 100644 index 0000000..48718bd --- /dev/null +++ b/cli/ai/application/docs-pattern.ts @@ -0,0 +1,68 @@ +/** + * Parse an AI documentation response into gitpagedocs pages. + * + * The model is instructed (see GITPAGEDOCS_DOC_SYSTEM_PROMPT) to delimit each + * page with a line `=== PAGE: <slug> | <Title> ===`. This splits that response + * into individual pages. If the model ignored the format, the whole response is + * returned as a single "ai-overview" page so content is never lost. + */ + +export interface AiDocPage { + /** lowercase-kebab, English, identical across languages (used as the filename) */ + slug: string; + title: string; + body: string; +} + +const PAGE_DELIMITER = /^===\s*PAGE:\s*(.+?)\s*\|\s*(.*?)\s*===\s*$/; + +/** Turn an arbitrary heading into a safe lowercase-kebab slug. */ +export function slugify(raw: string): string { + const slug = (raw ?? "") + .toLowerCase() + .normalize("NFKD") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 60); + return slug || "page"; +} + +export function parseAiPages(markdown: string): AiDocPage[] { + const text = (markdown ?? "").replace(/\r\n/g, "\n").trim(); + if (!text) return []; + + const pages: AiDocPage[] = []; + let current: { slug: string; title: string; body: string[] } | null = null; + + const flush = () => { + if (current) { + pages.push({ slug: current.slug, title: current.title, body: current.body.join("\n").trim() }); + } + }; + + for (const line of text.split("\n")) { + const match = line.match(PAGE_DELIMITER); + if (match) { + flush(); + const rawSlug = match[1].trim(); + const title = match[2].trim() || rawSlug; + current = { slug: slugify(rawSlug), title, body: [] }; + } else if (current) { + current.body.push(line); + } + } + flush(); + + const withBody = pages.filter((page) => page.body.length > 0); + if (withBody.length === 0) { + return [{ slug: "ai-overview", title: "Overview", body: text }]; + } + + // De-duplicate slugs within a single response (suffix later collisions). + const seen = new Map<string, number>(); + return withBody.map((page) => { + const count = seen.get(page.slug) ?? 0; + seen.set(page.slug, count + 1); + return count === 0 ? page : { ...page, slug: `${page.slug}-${count + 1}` }; + }); +} diff --git a/cli/ai/application/run-ai-cli-command.ts b/cli/ai/application/run-ai-cli-command.ts index 78549e5..a82f028 100644 --- a/cli/ai/application/run-ai-cli-command.ts +++ b/cli/ai/application/run-ai-cli-command.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import { AiCommandService } from "./ai-command"; import { createAiProvider } from "./ai-provider-factory"; import { FileSystemAdapter, type FilePayload } from "../infrastructure/file-system-adapter"; @@ -7,6 +6,11 @@ import { } from "../infrastructure/ai-config-file"; import type { AiCliRunPlan, AiCliRunSummary } from "../core/models/ai-cli-config"; import { promptMissingDirectories, runAiInteractivePrompt } from "../presentation/ai-prompts"; +import { GITPAGEDOCS_DOC_SYSTEM_PROMPT } from "../config"; +import { parseAiPages, type AiDocPage } from "./docs-pattern"; +import { writeVersionDocs } from "../infrastructure/version-docs-writer"; +// @ts-expect-error .mjs runtime module is type-less in this package. +import { DOC_VERSIONS } from "../../contracts/doc-versions.mjs"; const LANGUAGE_TO_LABEL: Record<string, string> = { pt: "Portuguese", @@ -22,7 +26,13 @@ function aggregateFilesPayload(files: FilePayload[]): string { function buildLanguagePrompt(basePrompt: string, language: "pt" | "en" | "es"): string { const languageLabel = LANGUAGE_TO_LABEL[language] ?? language; - return `${basePrompt}\n\nMandatory output rules:\n- Respond 100% in ${languageLabel}.\n- Generate professional markdown for technical documentation.\n- Include architectural overview, layer responsibilities, and next steps.\n- Avoid content outside the scope of the analyzed files.`; + const authorGuidance = basePrompt?.trim() ? `Additional author guidance:\n${basePrompt.trim()}\n\n` : ""; + return ( + `${GITPAGEDOCS_DOC_SYSTEM_PROMPT}\n\n` + + authorGuidance + + `Output language: write every page Title and body 100% in ${languageLabel}. ` + + `Keep each page slug lowercase-kebab and in English so the languages line up.` + ); } async function collectFilesWithInteractiveFallback( @@ -66,7 +76,7 @@ async function collectFilesWithInteractiveFallback( }; } -async function runPlan(plan: AiCliRunPlan): Promise<AiCliRunSummary> { +async function runPlan(plan: AiCliRunPlan, cwd: string): Promise<AiCliRunSummary> { const provider = createAiProvider({ provider: plan.config.ai.provider, model: plan.config.ai.model, @@ -92,30 +102,33 @@ async function runPlan(plan: AiCliRunPlan): Promise<AiCliRunSummary> { } const payload = aggregateFilesPayload(files); - const outputs: string[] = []; + const pagesByLang: Partial<Record<"pt" | "en" | "es", AiDocPage[]>> = {}; for (const language of plan.config.ai.languages) { const prompt = buildLanguagePrompt(plan.config.ai.contextPrompt, language); const markdown = await aiService.runGeneration(payload, prompt); - const outputFile = path.posix.join( - plan.config.ai.outputDir.replace(/\\/g, "/"), - `${plan.config.ai.filePrefix}-${plan.config.ai.provider}-${language}.md`, - ); - await fileSystem.writeDocumentationOutput(markdown, outputFile); - outputs.push(outputFile); + pagesByLang[language] = parseAiPages(markdown); } + // Wire the AI pages into the latest documentation version (gitpagedocs pattern). + const versionId = DOC_VERSIONS[DOC_VERSIONS.length - 1] as string; + const result = await writeVersionDocs({ cwd, versionId, pagesByLang }); + return { scannedDirectories: scanned, skippedDirectories: skipped, scannedFilesCount: files.length, - outputs, + outputs: result.files, + pages: result.slugs, }; } export async function runAiCliCommand(options: { cwd: string; onInfo?: (message: string) => void; + /** Provided by the composition root to scaffold the base gitpagedocs/ tree + * (so the version config.json exists before AI pages are wired into it). */ + onScaffold?: () => Promise<void>; }): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> { const logInfo = options.onInfo ?? (() => undefined); const configRepo = new AiConfigFileRepository(options.cwd); @@ -128,7 +141,14 @@ export async function runAiCliCommand(options: { logInfo(`[gitpagedocs:ai] Configuration saved to ${configRepo.getConfigPath()}`); } - const summary = await runPlan(plan); + // Build the base gitpagedocs structure BEFORE generation so the version + // config.json exists and the AI pages can be wired into it. + if (plan.runConfigScaffold && options.onScaffold) { + logInfo("[gitpagedocs] Generating base gitpagedocs structure..."); + await options.onScaffold(); + } + + const summary = await runPlan(plan, options.cwd); return { summary, runConfigScaffold: plan.runConfigScaffold, diff --git a/cli/ai/config.ts b/cli/ai/config.ts index 3868734..2957eb8 100644 --- a/cli/ai/config.ts +++ b/cli/ai/config.ts @@ -19,4 +19,32 @@ export const AI_MODEL_DEFAULTS: Readonly<Record<AiProviderId, string>> = { export const DEFAULT_AI_DOC_PROMPT = 'You are a senior tech writer. Describe the provided code.'; +/** + * System prompt that teaches the model what gitpagedocs is and forces it to + * return documentation in the gitpagedocs pattern: a sequence of pages, each + * introduced by a machine-parseable delimiter line so the CLI can split the + * response into individual versioned markdown pages and wire them into the + * viewer's config.json (routes-md + menus-header-md). + */ +export const GITPAGEDOCS_DOC_SYSTEM_PROMPT = [ + 'You are the documentation engine for "gitpagedocs", a generator of multilingual,', + 'versioned static documentation sites. gitpagedocs renders markdown pages from', + 'gitpagedocs/docs/versions/<version>/<lang>/<page>.md and wires them into a viewer', + 'through a config.json contract (routes + header menus). Your job is to analyze the', + 'provided source code and produce clean, professional documentation that fits this', + 'pattern so it can be dropped straight into a gitpagedocs site.', + '', + 'STRICT OUTPUT FORMAT — return ONLY a sequence of pages. Begin every page with a', + 'delimiter line on its own, exactly:', + '=== PAGE: <slug> | <Title> ===', + 'followed by the page body in GitHub-flavored markdown. Rules:', + '- Produce 4 to 8 cohesive pages. Suggested slugs: overview, getting-started,', + ' architecture, configuration, usage, deployment (adapt to the project).', + '- <slug> MUST be lowercase-kebab-case and in ENGLISH, and identical across every', + ' language run, so the languages line up (only the Title and body are translated).', + '- Do NOT wrap the whole answer in a code fence. Do NOT add any preamble or epilogue', + ' outside the page blocks. The first line of your reply MUST be a "=== PAGE:" line.', + '- Base everything strictly on the analyzed files; do not invent features.', +].join('\n'); + export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434'; diff --git a/cli/ai/core/models/ai-cli-config.ts b/cli/ai/core/models/ai-cli-config.ts index bcb4e50..f1005fc 100644 --- a/cli/ai/core/models/ai-cli-config.ts +++ b/cli/ai/core/models/ai-cli-config.ts @@ -28,4 +28,6 @@ export interface AiCliRunSummary { skippedDirectories: string[]; scannedFilesCount: number; outputs: string[]; + /** Slugs of the gitpagedocs pages wired into the version config (if any). */ + pages?: string[]; } diff --git a/cli/ai/infrastructure/version-docs-writer.ts b/cli/ai/infrastructure/version-docs-writer.ts new file mode 100644 index 0000000..dd5c537 --- /dev/null +++ b/cli/ai/infrastructure/version-docs-writer.ts @@ -0,0 +1,143 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +// @ts-expect-error .mjs runtime module is type-less in this package. +import { buildMdRoute } from "../../builders/route-builders.mjs"; +import type { AiDocPage } from "../application/docs-pattern"; + +const CANONICAL_LANGS = ["pt", "en", "es"] as const; +type Lang = (typeof CANONICAL_LANGS)[number]; + +/** AI routes/menus use a high, dedicated id range so they never clash with the + * deterministic base routes (ids 1..N) and can be identified for idempotency. */ +const AI_ROUTE_ID_BASE = 1000; + +export interface VersionDocsWriteInput { + cwd: string; + versionId: string; + /** Pages generated per language (only the languages the user selected). */ + pagesByLang: Partial<Record<Lang, AiDocPage[]>>; +} + +export interface VersionDocsWriteResult { + configPath: string; + slugs: string[]; + files: string[]; +} + +interface LangContent { + title: string; + body: string; +} + +function indexBySlug(pages: AiDocPage[] | undefined): Map<string, AiDocPage> { + const map = new Map<string, AiDocPage>(); + for (const page of pages ?? []) { + if (!map.has(page.slug)) map.set(page.slug, page); + } + return map; +} + +/** + * Write AI-generated pages into the gitpagedocs versioned structure and wire + * them into the version config.json (routes-md + menus-header-md). Pages are + * written for ALL three canonical languages (missing languages reuse an + * available translation) so every route path stays valid. Idempotent: any prior + * `aiGenerated` routes/menus are replaced, never duplicated. The base config and + * its deterministic routes are preserved untouched. + */ +export async function writeVersionDocs(input: VersionDocsWriteInput): Promise<VersionDocsWriteResult> { + const { cwd, versionId, pagesByLang } = input; + const base = `gitpagedocs/docs/versions/${versionId}`; + + const perLang: Record<Lang, Map<string, AiDocPage>> = { + pt: indexBySlug(pagesByLang.pt), + en: indexBySlug(pagesByLang.en), + es: indexBySlug(pagesByLang.es), + }; + + // Ordered unique slug list (prefer en order, then pt, then es). + const order: string[] = []; + const seen = new Set<string>(); + for (const lang of ["en", "pt", "es"] as Lang[]) { + for (const page of pagesByLang[lang] ?? []) { + if (!seen.has(page.slug)) { + seen.add(page.slug); + order.push(page.slug); + } + } + } + if (order.length === 0) { + throw new Error("No AI pages to write."); + } + + const resolve = (slug: string): Record<Lang, LangContent> => { + const fallback = CANONICAL_LANGS.map((lang) => perLang[lang].get(slug)).find(Boolean); + const out = {} as Record<Lang, LangContent>; + for (const lang of CANONICAL_LANGS) { + const page = perLang[lang].get(slug) ?? fallback!; + out[lang] = { title: page.title, body: page.body }; + } + return out; + }; + + // 1) Write the markdown files (all 3 canonical languages). + const writtenFiles: string[] = []; + for (const slug of order) { + const content = resolve(slug); + for (const lang of CANONICAL_LANGS) { + const rel = `${base}/${lang}/${slug}.md`; + const abs = path.resolve(cwd, rel); + await fs.mkdir(path.dirname(abs), { recursive: true }); + await fs.writeFile(abs, `${content[lang].body}\n`, "utf-8"); + writtenFiles.push(rel); + } + } + + // 2) Patch the version config.json (raw JSON round-trip, preserve all keys). + const configRel = `${base}/config.json`; + const configAbs = path.resolve(cwd, configRel); + let raw: string; + try { + raw = await fs.readFile(configAbs, "utf-8"); + } catch { + throw new Error( + `Version config not found at ${configRel}. Run the base scaffold (\`gitpagedocs\`) first.`, + ); + } + const config = JSON.parse(raw) as Record<string, unknown>; + + const routesMd = Array.isArray(config["routes-md"]) ? (config["routes-md"] as Array<Record<string, unknown>>) : []; + const menusMd = Array.isArray(config["menus-header-md"]) ? (config["menus-header-md"] as Array<Record<string, unknown>>) : []; + const keptRoutes = routesMd.filter((route) => route?.aiGenerated !== true); + const keptMenus = menusMd.filter((menu) => menu?.aiGenerated !== true); + + const newRoutes: Array<Record<string, unknown>> = []; + const newMenus: Array<Record<string, unknown>> = []; + order.forEach((slug, index) => { + const content = resolve(slug); + const id = AI_ROUTE_ID_BASE + index + 1; + const pathByLang = { + pt: `${base}/pt/${slug}.md`, + en: `${base}/en/${slug}.md`, + es: `${base}/es/${slug}.md`, + }; + const titles = { pt: content.pt.title, en: content.en.title, es: content.es.title }; + const descriptions = { pt: "", en: "", es: "" }; + const route = buildMdRoute(versionId, id, pathByLang, titles, descriptions, {}) as Record<string, unknown>; + route.aiGenerated = true; + newRoutes.push(route); + newMenus.push({ + id, + aiGenerated: true, + pt: { title: content.pt.title, "path-click": pathByLang.pt }, + en: { title: content.en.title, "path-click": pathByLang.en }, + es: { title: content.es.title, "path-click": pathByLang.es }, + }); + }); + + config["routes-md"] = [...keptRoutes, ...newRoutes]; + config["menus-header-md"] = [...keptMenus, ...newMenus]; + await fs.writeFile(configAbs, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); + + return { configPath: configRel, slugs: order, files: writtenFiles }; +} diff --git a/cli/ai/presentation/ai-prompts.ts b/cli/ai/presentation/ai-prompts.ts index 484ac19..fde7bad 100644 --- a/cli/ai/presentation/ai-prompts.ts +++ b/cli/ai/presentation/ai-prompts.ts @@ -141,7 +141,7 @@ export async function promptUseExistingConfig(existingConfig: AiCliConfig): Prom if (!useExistingConfig) return null; const runConfigScaffold = await askConfirm( - 'After generating docs with AI, run the default gitpagedocs scaffolding?', + 'Generate the base gitpagedocs structure first, so the AI pages are wired into the docs viewer?', true, ); @@ -220,7 +220,7 @@ export async function runAiInteractivePrompt(existingConfig?: AiCliConfig | null ); const runConfigScaffold = await askConfirm( - 'After generating docs with AI, run the default gitpagedocs scaffolding?', + 'Generate the base gitpagedocs structure first, so the AI pages are wired into the docs viewer?', true, ); diff --git a/cli/builders/root-config-builder.mjs b/cli/builders/root-config-builder.mjs index d7dd25b..cfb49bd 100644 --- a/cli/builders/root-config-builder.mjs +++ b/cli/builders/root-config-builder.mjs @@ -11,7 +11,7 @@ export function buildRootConfig(options = {}) { const useOfficialLayouts = !useLocalLayoutConfig; const githubOwner = options.githubOwner; const githubRepo = options.githubRepo; - const repositorySearchHome = githubOwner && githubRepo ? false : true; + const repositorySearchHome = false; const renderingUrl = githubOwner && githubRepo ? `https://${githubOwner}.github.io/${githubRepo}/` @@ -44,6 +44,7 @@ export function buildRootConfig(options = {}) { repositorySearchHome, rendering: renderingUrl, AiChatEnabled: true, + docsAccess: { enabled: false, publicKey: "" }, langmenu: defaultLangMenu, }, VersionControl: { diff --git a/cli/data/default-site-config.mjs b/cli/data/default-site-config.mjs index 4127d55..c5a67a2 100644 --- a/cli/data/default-site-config.mjs +++ b/cli/data/default-site-config.mjs @@ -114,6 +114,15 @@ export function getDefaultSiteConfig(DOCS, projectLink) { IconNavMenuBlockInactiveReactIconesTagSize: "25px", IconNavMenuBlockInactiveImgWidth: 20, IconNavMenuBlockInactiveImgHeight: 20, + IconDocsLockLightImg: "", + IconDocsLockDarkImg: "", + IconDocsLockReactIcones: true, + IconDocsLockReactIconesTag: "FiLock", + IconDocsLockReactIconesTagColorDark: "White", + IconDocsLockReactIconesTagColorLight: "black", + IconDocsLockReactIconesTagSize: "22px", + IconDocsLockImgWidth: 20, + IconDocsLockImgHeight: 20, RouteguideBrandPositionDefault: "center", RouteguideBrandContainerTopDefault: false, audioPlayerEnabled: true, diff --git a/cli/data/i18n-langmenu.mjs b/cli/data/i18n-langmenu.mjs index 6ca3b27..1278a9e 100644 --- a/cli/data/i18n-langmenu.mjs +++ b/cli/data/i18n-langmenu.mjs @@ -38,6 +38,67 @@ export const defaultLangMenu = { audioPopoverStatusPaused: "Pausado", audioPopoverStatusLoopOn: "Loop ativado", audioPopoverStatusLoopOff: "Loop desativado", + aiChatTitle: "Assistente de IA", + aiChatPlaceholder: "Pergunte sobre a documentação...", + aiChatConfigTitle: "Configurar IA", + aiChatConfigDesc: "Insira sua chave de API para ativar o chat.", + aiChatClearDataBtn: "Limpar dados locais", + aiChatSendBtn: "Enviar", + aiChatSaveStartBtn: "Salvar e iniciar conversa", + aiChatAttachAriaLabel: "Anexar arquivo de áudio ou imagem", + aiChatAttachedFilesLabel: "arquivo(s) anexado(s)", + aiChatRemoveAttachmentBtn: "Remover", + aiChatCancelResponseLabel: "Cancelar resposta", + aiChatOpenBtnAriaLabel: "Abrir chat de IA", + aiChatCloseBtnAriaLabel: "Fechar chat", + aiChatSystemPrompt: "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + aiChatEmptyStateGreeting: "Olá! Eu sou o assistente de inteligência artificial desta documentação. Você pode me fazer perguntas sobre o conteúdo ou buscar um termo em toda a documentação. Como posso ajudar você hoje?", + aiChatDisclaimer: "A IA pode cometer erros. Sempre verifique as informações.", + aiChatPasswordCreateDesc: "Crie uma senha local para criptografar suas chaves de API.", + aiChatPasswordUnlockDesc: "Digite sua senha local para desbloquear.", + aiChatPasswordPlaceholder: "Senha local", + aiChatCreatePasswordBtn: "Criar senha", + aiChatUnlockBtn: "Desbloquear", + aiChatWrongPassword: "Senha incorreta.", + aiChatLockBtn: "Bloquear", + aiChatResetBtn: "Esqueceu a senha?", + aiChatResetPopupTitle: "Redefinir senha?", + aiChatResetPopupDesc: "Isso apaga todas as chaves de API salvas e a senha local. Você criará uma nova senha na próxima vez.", + aiChatResetConfirmBtn: "Sim, redefinir", + aiChatResetCancelBtn: "Cancelar", + aiChatProviderLabel: "Provedor:", + aiChatApiKeyLabel: "Chave de API (deixe em branco para IA local):", + aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)", + aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)", + aiChatProviderGemini: "Google Gemini (1.5 Flash)", + aiChatProviderOllama: "Ollama Network (Local LLMs)", + aiChatEmptyPageContent: "Nenhuma página ativa compatível.", + aiChatNoPageId: "Sem página", + aiChatOllamaUrlLabel: "URL da API Ollama (deixe em branco para local)", + aiChatClearChatPopupTitle: "Limpar conversa?", + aiChatClearChatPopupDesc: "Isso apagará todo o histórico atual do chat. Esta ação não pode ser desfeita.", + aiChatClearChatConfirmBtn: "Sim, apagar", + aiChatClearChatCancelBtn: "Cancelar", + aiChatClearDataPopupTitle: "Apagar todos os dados?", + aiChatClearDataPopupDesc: "Isso apagará todos os chats e as chaves de API locais. O chat de IA será fechado.", + aiChatClearDataConfirmBtn: "Sim, apagar tudo", + aiChatClearDataCancelBtn: "Cancelar", + aiChatUserLabel: "Você", + aiChatApiError: "Ocorreu um erro de conexão com a API.", + aiChatError401: "Chave de acesso inválida ou provedor não autorizado.", + aiChatError429: "Limite de requisições excedido. Tente novamente mais tarde.", + aiChatError500: "Ocorreu um erro interno no servidor do modelo de IA.", + aiChatErrorGeneric: "Ocorreu um erro inesperado ao se comunicar com a IA.", + docsAccessGateTitle: "Documentação protegida", + docsAccessGateDescription: "Digite a senha ou a chave privada para acessar a documentação.", + docsAccessInputPlaceholder: "Senha ou chave privada", + docsAccessUnlockBtn: "Desbloquear", + docsAccessWrongCredential: "Senha ou chave incorreta.", + docsAccessLockTooltip: "Bloquear documentação", + docsAccessBlockPopupTitle: "Bloquear a documentação?", + docsAccessBlockPopupDesc: "Você precisará da senha novamente na próxima visita.", + docsAccessBlockConfirmBtn: "Sim, bloquear", + docsAccessBlockCancelBtn: "Cancelar", }, en: { pt: "Portuguese", @@ -78,6 +139,67 @@ export const defaultLangMenu = { audioPopoverStatusPaused: "Paused", audioPopoverStatusLoopOn: "Loop on", audioPopoverStatusLoopOff: "Loop off", + aiChatTitle: "AI Assistant", + aiChatPlaceholder: "Ask about the documentation...", + aiChatConfigTitle: "Configure AI", + aiChatConfigDesc: "Enter your API key to enable the chat.", + aiChatClearDataBtn: "Clear local data", + aiChatSendBtn: "Send", + aiChatSaveStartBtn: "Save & Start Chatting", + aiChatAttachAriaLabel: "Attach audio or image file", + aiChatAttachedFilesLabel: "file(s) attached", + aiChatRemoveAttachmentBtn: "Remove", + aiChatCancelResponseLabel: "Cancel response", + aiChatOpenBtnAriaLabel: "Open AI Chat", + aiChatCloseBtnAriaLabel: "Close chat", + aiChatSystemPrompt: "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + aiChatEmptyStateGreeting: "Hello! I am the artificial intelligence assistant of this documentation. You can ask me questions about the content or search for a term across the documentation. How can I help you today?", + aiChatDisclaimer: "AI can make mistakes. Always verify the information.", + aiChatPasswordCreateDesc: "Create a local password to encrypt your API keys.", + aiChatPasswordUnlockDesc: "Enter your local password to unlock.", + aiChatPasswordPlaceholder: "Local password", + aiChatCreatePasswordBtn: "Create password", + aiChatUnlockBtn: "Unlock", + aiChatWrongPassword: "Incorrect password.", + aiChatLockBtn: "Lock", + aiChatResetBtn: "Forgot password?", + aiChatResetPopupTitle: "Reset password?", + aiChatResetPopupDesc: "This erases all saved API keys and the local password. You'll create a new password next time.", + aiChatResetConfirmBtn: "Yes, reset", + aiChatResetCancelBtn: "Cancel", + aiChatProviderLabel: "Provider:", + aiChatApiKeyLabel: "API Key (leave blank for Local AI):", + aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)", + aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)", + aiChatProviderGemini: "Google Gemini (1.5 Flash)", + aiChatProviderOllama: "Ollama Network (Local LLMs)", + aiChatEmptyPageContent: "No compatible active page.", + aiChatNoPageId: "No page", + aiChatOllamaUrlLabel: "Ollama API URL (leave blank for local)", + aiChatClearChatPopupTitle: "Clear conversation?", + aiChatClearChatPopupDesc: "This will delete all current chat history. This action cannot be undone.", + aiChatClearChatConfirmBtn: "Yes, delete", + aiChatClearChatCancelBtn: "Cancel", + aiChatClearDataPopupTitle: "Erase all data?", + aiChatClearDataPopupDesc: "This will delete all chats and local API keys. The AI chat will be closed.", + aiChatClearDataConfirmBtn: "Yes, erase everything", + aiChatClearDataCancelBtn: "Cancel", + aiChatUserLabel: "You", + aiChatApiError: "An API connection error occurred.", + aiChatError401: "Invalid access key or unauthorized provider.", + aiChatError429: "Rate limit exceeded. Please try again later.", + aiChatError500: "An internal server error occurred on the AI model.", + aiChatErrorGeneric: "An unexpected error occurred while communicating with the AI.", + docsAccessGateTitle: "Protected documentation", + docsAccessGateDescription: "Enter the password or private key to view the documentation.", + docsAccessInputPlaceholder: "Password or private key", + docsAccessUnlockBtn: "Unlock", + docsAccessWrongCredential: "Incorrect password or key.", + docsAccessLockTooltip: "Lock documentation", + docsAccessBlockPopupTitle: "Block the documentation?", + docsAccessBlockPopupDesc: "You'll need the password again on your next visit.", + docsAccessBlockConfirmBtn: "Yes, block", + docsAccessBlockCancelBtn: "Cancel", }, es: { pt: "Portugues", @@ -118,5 +240,66 @@ export const defaultLangMenu = { audioPauseLabel: "Pausar música de fondo", audioPlaylistTitle: "Elegir pista", audioPlaylistDescription: "Seleccione una pista para reproducir de la playlist.", + aiChatTitle: "Asistente de IA", + aiChatPlaceholder: "Pregunta sobre la documentación...", + aiChatConfigTitle: "Configurar IA", + aiChatConfigDesc: "Introduce tu clave de API para activar el chat.", + aiChatClearDataBtn: "Borrar datos locales", + aiChatSendBtn: "Enviar", + aiChatSaveStartBtn: "Guardar y empezar a chatear", + aiChatAttachAriaLabel: "Adjuntar archivo de audio o imagen", + aiChatAttachedFilesLabel: "archivo(s) adjunto(s)", + aiChatRemoveAttachmentBtn: "Quitar", + aiChatCancelResponseLabel: "Cancelar respuesta", + aiChatOpenBtnAriaLabel: "Abrir chat de IA", + aiChatCloseBtnAriaLabel: "Cerrar chat", + aiChatSystemPrompt: "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + aiChatEmptyStateGreeting: "¡Hola! Soy el asistente de inteligencia artificial de esta documentación. Puedes hacerme preguntas sobre el contenido o buscar un término en toda la documentación. ¿Cómo puedo ayudarte hoy?", + aiChatDisclaimer: "La IA puede cometer errores. Verifica siempre la información.", + aiChatPasswordCreateDesc: "Crea una contraseña local para cifrar tus claves de API.", + aiChatPasswordUnlockDesc: "Introduce tu contraseña local para desbloquear.", + aiChatPasswordPlaceholder: "Contraseña local", + aiChatCreatePasswordBtn: "Crear contraseña", + aiChatUnlockBtn: "Desbloquear", + aiChatWrongPassword: "Contraseña incorrecta.", + aiChatLockBtn: "Bloquear", + aiChatResetBtn: "¿Olvidaste la contraseña?", + aiChatResetPopupTitle: "¿Restablecer contraseña?", + aiChatResetPopupDesc: "Esto borra todas las claves de API guardadas y la contraseña local. Crearás una nueva contraseña la próxima vez.", + aiChatResetConfirmBtn: "Sí, restablecer", + aiChatResetCancelBtn: "Cancelar", + aiChatProviderLabel: "Proveedor:", + aiChatApiKeyLabel: "Clave de API (déjalo en blanco para IA local):", + aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)", + aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)", + aiChatProviderGemini: "Google Gemini (1.5 Flash)", + aiChatProviderOllama: "Ollama Network (Local LLMs)", + aiChatEmptyPageContent: "Ninguna página activa compatible.", + aiChatNoPageId: "Sin página", + aiChatOllamaUrlLabel: "URL de la API de Ollama (déjalo en blanco para local)", + aiChatClearChatPopupTitle: "¿Borrar conversación?", + aiChatClearChatPopupDesc: "Esto eliminará todo el historial de chat actual. Esta acción no se puede deshacer.", + aiChatClearChatConfirmBtn: "Sí, borrar", + aiChatClearChatCancelBtn: "Cancelar", + aiChatClearDataPopupTitle: "¿Borrar todos los datos?", + aiChatClearDataPopupDesc: "Esto eliminará todos los chats y las claves de API locales. El chat de IA se cerrará.", + aiChatClearDataConfirmBtn: "Sí, borrar todo", + aiChatClearDataCancelBtn: "Cancelar", + aiChatUserLabel: "Tú", + aiChatApiError: "Se produjo un error de conexión con la API.", + aiChatError401: "Clave de acceso no válida o proveedor no autorizado.", + aiChatError429: "Límite de solicitudes superado. Inténtalo de nuevo más tarde.", + aiChatError500: "Se produjo un error interno del servidor en el modelo de IA.", + aiChatErrorGeneric: "Se produjo un error inesperado al comunicarse con la IA.", + docsAccessGateTitle: "Documentación protegida", + docsAccessGateDescription: "Introduce la contraseña o la clave privada para ver la documentación.", + docsAccessInputPlaceholder: "Contraseña o clave privada", + docsAccessUnlockBtn: "Desbloquear", + docsAccessWrongCredential: "Contraseña o clave incorrecta.", + docsAccessLockTooltip: "Bloquear documentación", + docsAccessBlockPopupTitle: "¿Bloquear la documentación?", + docsAccessBlockPopupDesc: "Tendrás que introducir la contraseña de nuevo en la próxima visita.", + docsAccessBlockConfirmBtn: "Sí, bloquear", + docsAccessBlockCancelBtn: "Cancelar", }, }; diff --git a/cli/data/layouts.mjs b/cli/data/layouts.mjs index 5df8664..f87bf33 100644 --- a/cli/data/layouts.mjs +++ b/cli/data/layouts.mjs @@ -348,6 +348,26 @@ export const LAYOUTS = [ supportsLightAndDarkModesReference: "slate-1", mode: "light", }, + { + id: "vscode-dark", + name: "VSCode Dark", + author: "Kauan Vidigal", + file: "templates/vscode-dark.json", + preview: "Visual Studio Code Dark+ theme with editor blue accents", + supportsLightAndDarkModes: true, + supportsLightAndDarkModesReference: "vscode-1", + mode: "dark", + }, + { + id: "vscode-light", + name: "VSCode Light", + author: "Kauan Vidigal", + file: "templates/vscode-light.json", + preview: "Visual Studio Code Light+ theme with clean editor tones", + supportsLightAndDarkModes: true, + supportsLightAndDarkModesReference: "vscode-1", + mode: "light", + }, ]; export const FALLBACK_LAYOUTS = LAYOUTS.filter((layout) => layout.id === "aurora-dark" || layout.id === "aurora-light"); \ No newline at end of file diff --git a/cli/data/theme-colors.mjs b/cli/data/theme-colors.mjs index c3e7025..80ceaa0 100644 --- a/cli/data/theme-colors.mjs +++ b/cli/data/theme-colors.mjs @@ -384,4 +384,26 @@ export const THEME_COLORS = { error: "#DC2626", success: "#16A34A", }, + "vscode-dark": { + background: "#1E1E1E", + primary: "#007ACC", + secondary: "#4EC9B0", + text: "#D4D4D4", + textSecondary: "#858585", + cardBackground: "#252526", + cardBorder: "#3C3C3C", + error: "#F14C4C", + success: "#4EC9B0", + }, + "vscode-light": { + background: "#FFFFFF", + primary: "#005FB8", + secondary: "#267F99", + text: "#1E1E1E", + textSecondary: "#6E6E6E", + cardBackground: "#F3F3F3", + cardBorder: "#E5E5E5", + error: "#CD3131", + success: "#098658", + }, }; \ No newline at end of file diff --git a/cli/index.mjs b/cli/index.mjs index 525ff91..2c57b35 100644 --- a/cli/index.mjs +++ b/cli/index.mjs @@ -8,9 +8,21 @@ const scriptPath = fileURLToPath(import.meta.url); const scriptDir = path.dirname(scriptPath); const tsEntry = path.join(scriptDir, "presentation", "index.ts"); +// Resolve tsx from THIS package's location, not the user's cwd. `node --import +// tsx` would otherwise resolve the bare "tsx" specifier relative to the current +// working directory, which fails when the CLI is installed globally and run +// from a project that doesn't depend on tsx (ERR_MODULE_NOT_FOUND 'tsx'). +let tsxLoader = "tsx"; +try { + tsxLoader = import.meta.resolve("tsx"); +} catch { + // Older Node without a stable import.meta.resolve — fall back to the bare + // specifier (works when tsx is resolvable from the cwd or globally). +} + const result = spawnSync( process.execPath, - ["--import", "tsx", tsEntry, ...process.argv.slice(2)], + ["--import", tsxLoader, tsEntry, ...process.argv.slice(2)], { stdio: "inherit", cwd: process.cwd(), diff --git a/cli/infrastructure/gitpagedocs-config-file.ts b/cli/infrastructure/gitpagedocs-config-file.ts new file mode 100644 index 0000000..6815f5b --- /dev/null +++ b/cli/infrastructure/gitpagedocs-config-file.ts @@ -0,0 +1,47 @@ +import fs from "node:fs/promises"; +import { defaultConfigLoader } from "@gitpagedocs/tools"; + +export interface DocsAccessConfig { + enabled: boolean; + publicKey: string; +} + +/** + * Reads and patches the generated gitpagedocs/config.json IN PLACE, preserving + * every existing key (raw-JSON round-trip — it never reserializes through a typed + * model that could drop unknown fields). Used by interactive commands that need + * to persist a single value (e.g. the documentation password public key). + */ +export class GitPageDocsConfigRepository { + constructor(private readonly cwd: string = process.cwd()) {} + + /** Absolute path to the JSON config, or throws a friendly, actionable error. */ + async resolvePath(): Promise<string> { + const resolved = await defaultConfigLoader.resolveConfigPath(this.cwd); + if (!resolved) { + throw new Error("gitpagedocs config.json not found. Run `gitpagedocs` first to generate it."); + } + if (!resolved.endsWith(".json")) { + throw new Error(`Expected a JSON config to patch, found "${resolved}". Use a gitpagedocs/config.json.`); + } + return resolved; + } + + /** + * Set `site.docsAccess = { enabled, publicKey }`, leaving all other config keys + * untouched. Returns the path that was written. + */ + async patchDocsAccess(publicKey: string, enabled = true): Promise<string> { + const configPath = await this.resolvePath(); + const raw = await fs.readFile(configPath, "utf-8"); + const config = JSON.parse(raw) as Record<string, unknown>; + const site = + config.site && typeof config.site === "object" + ? (config.site as Record<string, unknown>) + : {}; + site.docsAccess = { enabled, publicKey } satisfies DocsAccessConfig; + config.site = site; + await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); + return configPath; + } +} diff --git a/cli/package.json b/cli/package.json index e10a263..a69f0c0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@gitpagedocs/cli", - "version": "1.1.45", + "version": "1.1.53", "description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.", "bin": { "gitpagedocs": "index.mjs" @@ -29,8 +29,8 @@ }, "dependencies": { "@clack/prompts": "^1.5.1", - "@gitpagedocs/mcp": "workspace:*", - "@gitpagedocs/tools": "workspace:*", + "@gitpagedocs/mcp": "^1.1.53", + "@gitpagedocs/tools": "^1.1.53", "tsx": "^4.21.0" }, "repository": { diff --git a/cli/presentation/commands/pages.ts b/cli/presentation/commands/pages.ts index 8db0767..b2ee633 100644 --- a/cli/presentation/commands/pages.ts +++ b/cli/presentation/commands/pages.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import type { CommandContext } from "./run-command"; import { askConfirm } from "../ui/clack"; +import { askOwnerRepo, interactivePromptsAvailable } from "../ui/prompts"; // @ts-expect-error .mjs runtime module import { detectRepoFromGit, getCurrentGitBranch, tryConfigurePagesToGitHubActions } from "../../runtime/git-ops.mjs"; @@ -21,11 +22,14 @@ function readFlag(args: string[], name: string): string { * overwrite). Does NOT generate docs or push (that is `pages deploy`). */ export async function runPagesActions(ctx: CommandContext): Promise<void> { - const repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null; + let repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null; if (!repo) { - // eslint-disable-next-line no-console - console.log("\n Could not detect a GitHub repo from the git 'origin' remote.\n Add a remote or use `--push --owner <o> --repo <r>`.\n"); - return; + if (!interactivePromptsAvailable()) { + // eslint-disable-next-line no-console + console.log("\n Could not detect a GitHub repo from the git 'origin' remote.\n Add a remote or use `--push --owner <o> --repo <r>`.\n"); + return; + } + repo = await askOwnerRepo(null); } const branch = getCurrentGitBranch(ctx.cwd) as string; // eslint-disable-next-line no-console @@ -59,9 +63,14 @@ export async function runPagesDeploy(ctx: CommandContext): Promise<void> { } } if (!owner || !repo) { - // eslint-disable-next-line no-console - console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n"); - return; + if (!interactivePromptsAvailable()) { + // eslint-disable-next-line no-console + console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n"); + return; + } + const answered = await askOwnerRepo(null, { owner, repo }); + owner = answered.owner; + repo = answered.repo; } // eslint-disable-next-line no-console @@ -76,7 +85,8 @@ export async function runPagesDeploy(ctx: CommandContext): Promise<void> { return; } - const bin = path.join(ctx.pkgRoot, "cli", "index.mjs"); + // pkgRoot is the cli package root (cli/), where the bin lives as index.mjs. + const bin = path.join(ctx.pkgRoot, "index.mjs"); const pushArgs = ["--push", "--owner", owner, "--repo", repo, ...(docsPath ? ["--path", docsPath] : [])]; const result = spawnSync(process.execPath, [bin, ...pushArgs], { stdio: "inherit", cwd: ctx.cwd, env: process.env }); diff --git a/cli/presentation/commands/password.ts b/cli/presentation/commands/password.ts new file mode 100644 index 0000000..367978a --- /dev/null +++ b/cli/presentation/commands/password.ts @@ -0,0 +1,62 @@ +import { NodeCryptoService, deriveDocAccessKeys } from "@gitpagedocs/tools"; +import type { CommandContext } from "./run-command"; +import { askPassword, note, outro } from "../ui/clack"; +import { interactivePromptsAvailable } from "../ui/prompts"; +import { GitPageDocsConfigRepository } from "../../infrastructure/gitpagedocs-config-file"; + +const MIN_PASSWORD_LENGTH = 4; + +/** + * `gitpagedocs password` — interactively set a documentation access password. + * + * Type + confirm a password, derive the double-hash key pair, write the PUBLIC + * key into gitpagedocs/config.json (which makes the frontend gate the docs), and + * print the PRIVATE key for the user to copy. The password itself is never stored. + */ +export async function runPassword(ctx: CommandContext): Promise<void> { + if (!interactivePromptsAvailable()) { + // eslint-disable-next-line no-console + console.log("\n `gitpagedocs password` is interactive — run it in a terminal (TTY).\n"); + return; + } + + const password = await askPassword({ + message: "Documentation password:", + validate: (v) => + v && v.trim().length >= MIN_PASSWORD_LENGTH + ? undefined + : `Use at least ${MIN_PASSWORD_LENGTH} characters.`, + }); + const confirmation = await askPassword({ + message: "Confirm password:", + validate: (v) => (v ? undefined : "Required."), + }); + + if (password !== confirmation) { + note("Passwords do not match. Nothing was changed.", "Aborted"); + return; + } + + const { privateKey, publicKey } = await deriveDocAccessKeys(password, new NodeCryptoService()); + + let configPath: string; + try { + configPath = await new GitPageDocsConfigRepository(ctx.cwd).patchDocsAccess(publicKey); + } catch (error) { + note(error instanceof Error ? error.message : String(error), "Could not save"); + return; + } + + note( + `Public key saved to ${configPath}\nThe documentation is now password-protected.`, + "Saved", + ); + // eslint-disable-next-line no-console + console.log( + "\n PRIVATE KEY (copy & keep it safe — it also unlocks the docs):\n\n" + + ` ${privateKey}\n\n` + + " Readers can unlock with the password OR this private key.\n" + + " To remove protection, set site.docsAccess.enabled to false in config.json.\n", + ); + outro("Done."); +} diff --git a/cli/presentation/commands/run-command.ts b/cli/presentation/commands/run-command.ts index 1858eb2..fce3a92 100644 --- a/cli/presentation/commands/run-command.ts +++ b/cli/presentation/commands/run-command.ts @@ -21,6 +21,7 @@ import { runConfig } from "./config-info"; import { runMcp } from "./mcp"; import { runDocs } from "./docs"; import { runPagesActions, runPagesDeploy } from "./pages"; +import { runPassword } from "./password"; const REGISTRY: Record<string, CommandHandler> = { version: runVersion, @@ -34,6 +35,7 @@ const REGISTRY: Record<string, CommandHandler> = { config: runConfig, mcp: runMcp, docs: runDocs, + password: runPassword, }; /** Returns true if a new command handled the invocation (skip legacy flow). */ diff --git a/cli/presentation/index.ts b/cli/presentation/index.ts index 693b8f3..94f7554 100644 --- a/cli/presentation/index.ts +++ b/cli/presentation/index.ts @@ -78,11 +78,19 @@ async function main(): Promise<void> { }, runAi: async (context) => { console.log("[gitpagedocs] Iniciando Módulo de IA na CLI..."); + const safePrebuiltDir = + typeof context.prebuiltDir === "string" + ? context.prebuiltDir + : path.join(String(context.pkgRoot ?? process.cwd()), "prebuilt"); // @ts-ignore const { runAiCliCommand } = await import("../ai/application/run-ai-cli-command.ts"); const result = await runAiCliCommand({ cwd: process.cwd(), onInfo: (message: string) => console.log(message), + // Scaffold the base gitpagedocs/ structure before AI pages are wired in. + onScaffold: async () => { + await executeConfigOnly({ ...context, prebuiltDir: safePrebuiltDir }, configOnlyRuntime); + }, }); if (result.summary.scannedFilesCount === 0) { @@ -90,13 +98,13 @@ async function main(): Promise<void> { return; } - if (result.runConfigScaffold) { - console.log("\n[gitpagedocs] Scaffolding GitPageDocs ecosystem automatically..."); - const safePrebuiltDir = typeof context.prebuiltDir === "string" ? context.prebuiltDir : path.join(String(context.pkgRoot ?? process.cwd()), "prebuilt"); - await executeConfigOnly({ ...context, prebuiltDir: safePrebuiltDir }, configOnlyRuntime); - } - - console.log(`\n🎉 Processo completo! Arquivos gerados: ${result.summary.outputs.join(", ")}`); + const pages = result.summary.pages ?? []; + console.log( + `\n🎉 Processo completo! Páginas no padrão gitpagedocs: ${pages.join(", ") || "(nenhuma)"}`, + ); + console.log( + `[gitpagedocs:ai] ${result.summary.outputs.length} arquivos markdown gerados e conectados ao config.json da versão mais recente.`, + ); }, }); printCredits(); diff --git a/cli/presentation/options/resolver.ts b/cli/presentation/options/resolver.ts index e8bd991..cd218ff 100644 --- a/cli/presentation/options/resolver.ts +++ b/cli/presentation/options/resolver.ts @@ -1,7 +1,16 @@ import type { CliOptions } from "../../domain/models/cli-options"; import { parseCliOptions } from "./parser"; -import { shouldRunInteractive, promptConfigOnlyOptions, promptHomeOptions } from "../ui/prompts"; +import { + shouldRunInteractive, + promptConfigOnlyOptions, + promptHomeOptions, + promptDeployOptions, + ensureGitRepoInteractive, + interactivePromptsAvailable, +} from "../ui/prompts"; import { DEFAULTS } from "./schema"; +// @ts-expect-error .mjs runtime module is type-less in this package. +import { detectRepoFromGit } from "../../runtime/git-ops.mjs"; export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Promise<CliOptions> { const parsed = parseCliOptions(argv, env); @@ -11,6 +20,20 @@ export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Pr parsed.basePath = parsed.basePath ?? parsed.docsPath ?? DEFAULTS.home.basePath; } + // `deploy` / `--push` need owner + repo AND a git repo. If anything is + // missing, prompt interactively (owner/repo pre-filled from the git origin + // remote; offer to `git init`) instead of throwing. In CI/non-TTY we fall + // through so the explicit guards in the push flow still fire clearly. + if (parsed.shouldPush && interactivePromptsAvailable()) { + let resolved = parsed; + if (!parsed.githubOwner || !parsed.githubRepo) { + const detected = detectRepoFromGit(process.cwd()) as { owner: string; repo: string } | null; + resolved = await promptDeployOptions(parsed, detected); + } + await ensureGitRepoInteractive(process.cwd()); + return resolved; + } + const runHomeInteractive = parsed.isInteractive || (shouldRunInteractive(argv) && parsed.mode === "home"); if (runHomeInteractive && parsed.mode === "home") { return promptHomeOptions(parsed); diff --git a/cli/presentation/ui/clack.ts b/cli/presentation/ui/clack.ts index 8e0ed93..4540286 100644 --- a/cli/presentation/ui/clack.ts +++ b/cli/presentation/ui/clack.ts @@ -5,6 +5,7 @@ */ import { text, + password, confirm, select, multiselect, @@ -40,6 +41,22 @@ export async function askText(options: AskTextOptions): Promise<string> { return exitIfCancelled(result); } +export interface AskPasswordOptions { + message: string; + validate?: (value: string) => string | undefined; +} + +/** Masked password prompt (input hidden as the user types). */ +export async function askPassword(options: AskPasswordOptions): Promise<string> { + const result = await password({ + message: options.message, + validate: options.validate + ? (value) => options.validate?.(value ?? "") + : undefined, + }); + return exitIfCancelled(result); +} + export async function askConfirm(message: string, initialValue = false): Promise<boolean> { const result = await confirm({ message, initialValue }); return exitIfCancelled(result); diff --git a/cli/presentation/ui/prompts.ts b/cli/presentation/ui/prompts.ts index 01e62e4..e63cbdb 100644 --- a/cli/presentation/ui/prompts.ts +++ b/cli/presentation/ui/prompts.ts @@ -1,6 +1,9 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { execSync } from "node:child_process"; import type { CliOptions } from "../../domain/models/cli-options"; import { DEFAULTS } from "../options/schema"; -import { askText, askConfirm } from "./clack"; +import { askText, askConfirm, note } from "./clack"; function isCiOrNonTty(): boolean { if (process.env.CI === "true") return true; @@ -9,6 +12,11 @@ function isCiOrNonTty(): boolean { return false; } +/** True when clack prompts can run (interactive TTY, not CI). */ +export function interactivePromptsAvailable(): boolean { + return !isCiOrNonTty(); +} + export function shouldRunInteractive(argv: string[]): boolean { if (isCiOrNonTty()) return false; const args = argv.slice(2); @@ -41,6 +49,72 @@ export async function promptHomeOptions(parsed: CliOptions): Promise<CliOptions> }; } +/** + * Deploy/--push needs a GitHub owner + repo. When they are missing, prompt for + * them (pre-filled from the git `origin` remote when available) instead of + * crashing with "`--push` requires owner and repo". + */ +export async function askOwnerRepo( + detected: { owner: string; repo: string } | null, + defaults?: { owner?: string; repo?: string }, +): Promise<{ owner: string; repo: string }> { + const owner = await askText({ + message: "GitHub owner (user or organization):", + defaultValue: defaults?.owner || detected?.owner || "", + validate: (v) => (v && v.trim() ? undefined : "Owner is required."), + }); + const repo = await askText({ + message: "GitHub repository name:", + defaultValue: defaults?.repo || detected?.repo || "", + validate: (v) => (v && v.trim() ? undefined : "Repository is required."), + }); + return { owner: owner.trim(), repo: repo.trim() }; +} + +export async function promptDeployOptions( + parsed: CliOptions, + detected: { owner: string; repo: string } | null, +): Promise<CliOptions> { + note( + "Deploy publishes gitpagedocs to GitHub Pages and needs the target\nrepository (owner/repo). Press Enter to accept a detected value.", + "GitHub Pages deploy", + ); + const { owner, repo } = await askOwnerRepo(detected, { + owner: parsed.githubOwner, + repo: parsed.githubRepo, + }); + + return { + ...parsed, + githubOwner: owner, + githubRepo: repo, + }; +} + +/** + * Deploy/--push needs a git repository in the current folder. When there isn't + * one, offer to run `git init` instead of crashing with "not a git repository". + * In CI/non-TTY this is a no-op so the downstream guard still reports clearly. + */ +export async function ensureGitRepoInteractive(root: string): Promise<void> { + if (existsSync(path.join(root, ".git"))) return; + if (!interactivePromptsAvailable()) return; + const init = await askConfirm( + "This folder is not a git repository yet. Initialize one here (git init)?", + true, + ); + if (!init) { + note("Deploy needs a git repository. Run `git init`, then retry.", "Heads up"); + return; + } + try { + execSync("git init", { cwd: root, stdio: "ignore" }); + note("Initialized an empty git repository.", "git"); + } catch { + note("Could not run `git init`. Initialize git manually, then retry.", "git"); + } +} + export async function promptConfigOnlyOptions(parsed: CliOptions): Promise<CliOptions> { const useLocalLayoutConfig = await askConfirm( "Generate local layout templates?", diff --git a/frontend/README.md b/frontend/README.md index 3c13636..34fa708 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -45,6 +45,10 @@ Two independent surfaces share the same encrypted vault from `@gitpagedocs/tools Both are gated by a **local password**: it creates/unlocks an AES-256-GCM vault (`src/shared/lib/ai-secure-storage.ts` → `EncryptedCredentialVault`), keys are stored encrypted in `localStorage` (`gitpagedocs:vault`) and decrypted only in-session. The legacy plaintext key (`gitpagedocs_ai_key`) is migrated and wiped on first unlock. The chat itself runs through the shared 14-provider AI core, so the service layer never reads keys from storage — credentials are injected per request. +## Documentation access gate + +When `site.docsAccess.enabled` is set in `gitpagedocs/config.json` (via the `gitpagedocs password` CLI command), the whole documentation is blocked behind a full-page gate (`src/features/docs-access`). Visitors unlock with the **password or the private key**, verified against the stored public key with `verifyDocAccess` from `@gitpagedocs/tools/crypto/web` (double-hash SHA-256). The unlock is cached in `localStorage` (only the public hash), and a lock button in the sidebar re-blocks by clearing it. All gate/chat strings are sourced from `config.json` `langmenu` (en/pt/es). + ## Environment `.env` (frontend-local; copy from `.env.example`): diff --git a/frontend/src/entities/docs/api/load-remote-docs-data-client.ts b/frontend/src/entities/docs/api/load-remote-docs-data-client.ts index 8f6c24b..87cd508 100644 --- a/frontend/src/entities/docs/api/load-remote-docs-data-client.ts +++ b/frontend/src/entities/docs/api/load-remote-docs-data-client.ts @@ -28,6 +28,7 @@ import { fetchRepoJson, fetchUrlJson, } from "@/shared/api/fetch-client"; +import { withConfigDefaults } from "../lib/with-config-defaults"; type VersionConfig = { routes?: RouteConfig[]; @@ -280,10 +281,13 @@ export async function loadRemoteDocsData( selectedVersionId?: string, selectedLanguage: SupportedLanguage = "en", ): Promise<LoadedDocsData | null> { - const config = await fetchRepoJson<GitPageDocsConfig>(owner, repo, "gitpagedocs/config.json"); - if (!config) { + const rawConfig = await fetchRepoJson<GitPageDocsConfig>(owner, repo, "gitpagedocs/config.json"); + if (!rawConfig) { return null; } + // Backfill the `site` section so OLD config.json files inherit the current + // config.json defaults (header control icons, en/pt/es langmenu, language). + const config = withConfigDefaults(rawConfig); const versions = dedupeVersionEntriesById(config.VersionControl?.versions ?? []); const activeVersion = resolveActiveVersion(versions, selectedVersionId, config.site.docsVersion); diff --git a/frontend/src/entities/docs/api/resolve-docs-source.ts b/frontend/src/entities/docs/api/resolve-docs-source.ts index fd002d5..8f6b0d2 100644 --- a/frontend/src/entities/docs/api/resolve-docs-source.ts +++ b/frontend/src/entities/docs/api/resolve-docs-source.ts @@ -2,6 +2,7 @@ import type { GitPageDocsConfig } from "@/entities/docs/model/types"; import { readRemoteJsonFromRepo } from "./io/remote-fetcher"; import { parseOwnerRepoFromRenderingUrl } from "./utils/url-utils"; import { DEFAULT_CONFIG_PATH } from "@/shared/config/constants"; +import { withConfigDefaults } from "../lib/with-config-defaults"; export interface ResolvedDocsSource { source: "local" | "remote"; @@ -91,7 +92,9 @@ export async function resolveDocsSource( source, owner, repo, - config, + // Backfill the `site` section so OLD config.json files inherit the current + // config.json defaults (header control icons, en/pt/es langmenu, language). + config: withConfigDefaults(config), hasGitPageDocs, showRepositorySearchHome, isRepositoryRouteRequest, diff --git a/frontend/src/entities/docs/lib/with-config-defaults.ts b/frontend/src/entities/docs/lib/with-config-defaults.ts new file mode 100644 index 0000000..c4af84e --- /dev/null +++ b/frontend/src/entities/docs/lib/with-config-defaults.ts @@ -0,0 +1,61 @@ +import type { GitPageDocsConfig, SiteConfig, UiTranslationsConfig } from "@/entities/docs/model/types"; +import siteBaseline from "@/shared/config/site-baseline.json"; +import translationsBaseline from "@/shared/config/translations-baseline.json"; + +/** + * Baseline `site` defaults, generated from the canonical gitpagedocs/config.json + * (see tools/gen-site-baseline.mjs). OLD config.json files lack newer `site` fields + * — react icon flags/tags for the header controls and the en/pt/es langmenu entries + * the AI chat reads. Deep-merging them UNDER the loaded config makes any missing + * field inherit the exact value shipped in the current config.json. + */ +export const SITE_CONFIG_DEFAULTS = siteBaseline as unknown as SiteConfig; + +/** + * Baseline `translations` defaults (navigation/footer/notFound en/pt/es text), + * generated from the canonical gitpagedocs/config.json. OLD config.json files often + * omit the whole `translations` section, which left prev/next/menu/footer labels + * falling back to hardcoded English. Merging this baseline restores pt/es text. + */ +export const TRANSLATIONS_CONFIG_DEFAULTS = translationsBaseline as unknown as UiTranslationsConfig; + +function isPlainObject(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Deep-merges `override` onto `base`, where `base` provides the defaults. + * - Nested plain objects (e.g. langmenu.pt) merge recursively so missing keys are filled. + * - Arrays and scalars from `override` replace the base wholesale when present. + * - `undefined` keys in `override` are ignored (the base default is kept). + */ +function deepMergeDefaults<T>(base: T, override: unknown): T { + if (!isPlainObject(base)) { + return override === undefined ? base : (override as T); + } + if (!isPlainObject(override)) { + return base; + } + const out: Record<string, unknown> = { ...base }; + for (const key of Object.keys(override)) { + const overrideValue = override[key]; + if (overrideValue === undefined) continue; + const baseValue = (base as Record<string, unknown>)[key]; + out[key] = + isPlainObject(baseValue) && isPlainObject(overrideValue) + ? deepMergeDefaults(baseValue, overrideValue) + : overrideValue; + } + return out as T; +} + +/** + * Returns a config whose `site` and `translations` sections are backfilled with the + * baseline defaults. Only chrome defaults are merged — routes, menus, auth and + * VersionControl come from the loaded config untouched (they are deployment content). + */ +export function withConfigDefaults(config: GitPageDocsConfig): GitPageDocsConfig { + const mergedSite = deepMergeDefaults(SITE_CONFIG_DEFAULTS, config?.site); + const mergedTranslations = deepMergeDefaults(TRANSLATIONS_CONFIG_DEFAULTS, config?.translations); + return { ...config, site: mergedSite, translations: mergedTranslations }; +} diff --git a/frontend/src/entities/docs/model/types/site.ts b/frontend/src/entities/docs/model/types/site.ts index 29b165d..d5e0266 100644 --- a/frontend/src/entities/docs/model/types/site.ts +++ b/frontend/src/entities/docs/model/types/site.ts @@ -135,6 +135,16 @@ export interface SiteConfig { IconSidebarExpandReactIconesTagSize?: string; IconSidebarExpandImgWidth?: string | number; IconSidebarExpandImgHeight?: string | number; + /** Documentation lock button (clears the access cache to re-block the docs) */ + IconDocsLockLightImg?: string; + IconDocsLockDarkImg?: string; + IconDocsLockReactIcones?: boolean; + IconDocsLockReactIconesTag?: string; + IconDocsLockReactIconesTagColorDark?: string; + IconDocsLockReactIconesTagColorLight?: string; + IconDocsLockReactIconesTagSize?: string; + IconDocsLockImgWidth?: string | number; + IconDocsLockImgHeight?: string | number; /** Block menu on nav toggle: active (blocking) state icon */ IconNavMenuBlockActiveLightImg?: string; IconNavMenuBlockActiveDarkImg?: string; @@ -256,6 +266,13 @@ export interface SiteConfig { /** AI Chat toggle: enable/disable entirely */ AiChatEnabled?: boolean; + /** Documentation-wide password gate. When enabled with a publicKey, the + * frontend blocks all docs until the visitor enters the password or private key. */ + docsAccess?: { + enabled?: boolean; + publicKey?: string; + }; + /** AI Chat toggle: open icon */ IconAiChatOpenLightImg?: string; IconAiChatOpenDarkImg?: string; diff --git a/frontend/src/features/ask-ai/ui/api-key-form.tsx b/frontend/src/features/ask-ai/ui/api-key-form.tsx index b689571..8b7a72e 100644 --- a/frontend/src/features/ask-ai/ui/api-key-form.tsx +++ b/frontend/src/features/ask-ai/ui/api-key-form.tsx @@ -80,7 +80,7 @@ export const ApiKeyForm: React.FC<ApiKeyFormProps> = ({ onSave, labels }) => { data-testid="drawer-save-key" className={styles.btnPrimary} > - {labels?.aiChatSendBtn || "Save & Start Chatting"} + {labels?.aiChatSaveStartBtn || "Save & Start Chatting"} </button> </div> </form> diff --git a/frontend/src/features/docs-access/index.ts b/frontend/src/features/docs-access/index.ts new file mode 100644 index 0000000..cb4ebdd --- /dev/null +++ b/frontend/src/features/docs-access/index.ts @@ -0,0 +1,10 @@ +export { useDocsAccess } from "./model/use-docs-access"; +export type { + DocsAccessState, + DocsAccessConfigInput, + UseDocsAccessResult, +} from "./model/use-docs-access"; +export { DocsAccessGate } from "./ui/docs-access-gate"; +export type { DocsAccessTexts } from "./ui/docs-access-gate"; +export { DocsLockButton } from "./ui/docs-lock-button"; +export type { DocsLockTexts } from "./ui/docs-lock-button"; diff --git a/frontend/src/features/docs-access/model/use-docs-access.ts b/frontend/src/features/docs-access/model/use-docs-access.ts new file mode 100644 index 0000000..bc1f6df --- /dev/null +++ b/frontend/src/features/docs-access/model/use-docs-access.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useState } from "react"; +import { WebCryptoService, verifyDocAccess } from "@gitpagedocs/tools/crypto/web"; + +export type DocsAccessState = "loading" | "locked" | "unlocked"; + +export interface DocsAccessConfigInput { + enabled?: boolean; + publicKey?: string; +} + +export interface UseDocsAccessResult { + /** Whether the gate is enabled at all (config has enabled + publicKey). */ + enabled: boolean; + state: DocsAccessState; + /** Verify a password OR private key; persists + unlocks on success. */ + unlock: (input: string) => Promise<boolean>; + /** Clear the cached unlock so the next visit re-blocks. */ + lockAgain: () => void; +} + +/** Same convention as use-docs-preferences buildStorageKey, scoped to this site. */ +function buildStorageKey(siteName: string): string { + return `git-page-docs:docs-access:${siteName.toLowerCase().replaceAll(" ", "-")}`; +} + +function readCache(storageKey: string): string | null { + if (typeof window === "undefined") return null; + try { + return window.localStorage.getItem(storageKey); + } catch { + return null; + } +} + +function writeCache(storageKey: string, value: string): void { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(storageKey, value); + } catch { + // Ignore storage failures (private mode, blocked storage). + } +} + +function clearCache(storageKey: string): void { + if (typeof window === "undefined") return; + try { + window.localStorage.removeItem(storageKey); + } catch { + // Ignore storage failures. + } +} + +/** + * Documentation-wide access gate state. Backward compatible: when the gate is + * disabled or no public key is configured, the state is "unlocked" (docs open). + * The cache stores ONLY the public hash; the password/private key is never + * persisted, and a changed config public key invalidates a stale cache. + */ +export function useDocsAccess( + config: DocsAccessConfigInput | undefined, + siteName: string, +): UseDocsAccessResult { + const enabled = Boolean(config?.enabled); + const publicKey = (config?.publicKey ?? "").trim(); + const gateActive = enabled && publicKey.length > 0; + const storageKey = buildStorageKey(siteName); + const [state, setState] = useState<DocsAccessState>("loading"); + + useEffect(() => { + if (!gateActive) { + setState("unlocked"); + return; + } + const cached = readCache(storageKey); + setState(cached && cached === publicKey ? "unlocked" : "locked"); + }, [gateActive, publicKey, storageKey]); + + const unlock = useCallback( + async (input: string): Promise<boolean> => { + if (!publicKey) return false; + const ok = await verifyDocAccess(input.trim(), publicKey, new WebCryptoService()); + if (!ok) return false; + writeCache(storageKey, publicKey); + setState("unlocked"); + return true; + }, + [publicKey, storageKey], + ); + + const lockAgain = useCallback(() => { + clearCache(storageKey); + setState("locked"); + }, [storageKey]); + + return { enabled: gateActive, state, unlock, lockAgain }; +} diff --git a/frontend/src/features/docs-access/ui/docs-access-gate.module.css b/frontend/src/features/docs-access/ui/docs-access-gate.module.css new file mode 100644 index 0000000..fe23474 --- /dev/null +++ b/frontend/src/features/docs-access/ui/docs-access-gate.module.css @@ -0,0 +1,98 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 3000; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + background: rgba(8, 10, 16, 0.92); + backdrop-filter: blur(6px); +} + +.card { + width: 100%; + max-width: 380px; + display: flex; + flex-direction: column; + gap: 0.85rem; + padding: 2rem 1.75rem; + border-radius: 16px; + background: #ffffff; + color: #111827; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45); + text-align: center; +} + +.title { + margin: 0; + font-size: 1.35rem; + font-weight: 700; +} + +.description { + margin: 0; + font-size: 0.92rem; + line-height: 1.5; + color: #4b5563; +} + +.input { + width: 100%; + padding: 0.7rem 0.85rem; + border: 1px solid #d1d5db; + border-radius: 10px; + font-size: 0.95rem; + outline: none; + box-sizing: border-box; +} + +.input:focus { + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); +} + +.error { + margin: 0; + font-size: 0.85rem; + color: #dc2626; +} + +.button { + width: 100%; + padding: 0.7rem 1rem; + border: none; + border-radius: 10px; + background: #2563eb; + color: #ffffff; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, opacity 0.15s ease; +} + +.button:hover:not(:disabled) { + background: #1d4ed8; +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +@media (prefers-color-scheme: dark) { + .card { + background: #11151c; + color: #f3f4f6; + } + + .description { + color: #9ca3af; + } + + .input { + background: #0b0e13; + border-color: #374151; + color: #f3f4f6; + } +} diff --git a/frontend/src/features/docs-access/ui/docs-access-gate.tsx b/frontend/src/features/docs-access/ui/docs-access-gate.tsx new file mode 100644 index 0000000..b4742b3 --- /dev/null +++ b/frontend/src/features/docs-access/ui/docs-access-gate.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useState, type FormEvent } from "react"; +import styles from "./docs-access-gate.module.css"; + +export interface DocsAccessTexts { + title: string; + description: string; + placeholder: string; + unlockBtn: string; + wrongCredential: string; +} + +interface DocsAccessGateProps { + texts: DocsAccessTexts; + onUnlock: (input: string) => Promise<boolean>; +} + +/** Full-page gate shown when the documentation is locked behind a password. */ +export function DocsAccessGate({ texts, onUnlock }: DocsAccessGateProps) { + const [value, setValue] = useState(""); + const [error, setError] = useState(false); + const [busy, setBusy] = useState(false); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + if (busy || !value.trim()) return; + setBusy(true); + const ok = await onUnlock(value); + setBusy(false); + setError(!ok); + }; + + return ( + <div className={styles.overlay} role="dialog" aria-modal="true" aria-label={texts.title}> + <form className={styles.card} onSubmit={handleSubmit}> + <h1 className={styles.title}>{texts.title}</h1> + <p className={styles.description}>{texts.description}</p> + <input + className={styles.input} + type="password" + value={value} + onChange={(event) => { + setValue(event.target.value); + setError(false); + }} + placeholder={texts.placeholder} + autoFocus + autoComplete="off" + /> + {error && <p className={styles.error}>{texts.wrongCredential}</p>} + <button className={styles.button} type="submit" disabled={busy || !value.trim()}> + {texts.unlockBtn} + </button> + </form> + </div> + ); +} diff --git a/frontend/src/features/docs-access/ui/docs-lock-button.tsx b/frontend/src/features/docs-access/ui/docs-lock-button.tsx new file mode 100644 index 0000000..e9aeebd --- /dev/null +++ b/frontend/src/features/docs-access/ui/docs-lock-button.tsx @@ -0,0 +1,66 @@ +"use client"; + +import Image from "next/image"; +import { useState } from "react"; +import { ReactIconByTag } from "@/shared/ui/react-icon-by-tag"; +import { ConfirmPopup } from "@/shared/ui/confirm-popup/confirm-popup"; +import type { ResolvedNavMenuIconConfig } from "@/shared/lib/icons/nav-menu/resolve-nav-menu-icon"; + +export interface DocsLockTexts { + tooltip: string; + popupTitle: string; + popupDescription: string; + confirmText: string; + cancelText: string; +} + +interface DocsLockButtonProps { + icon: ResolvedNavMenuIconConfig; + texts: DocsLockTexts; + onConfirmBlock: () => void; + className?: string; +} + +/** Menu button that re-locks the documentation (clears the unlock cache) after + * a confirmation popup. Reuses the shared ConfirmPopup and icon resolver. */ +export function DocsLockButton({ icon, texts, onConfirmBlock, className }: DocsLockButtonProps) { + const [isPopupOpen, setIsPopupOpen] = useState(false); + + return ( + <> + <button + type="button" + className={className} + onClick={() => setIsPopupOpen(true)} + aria-label={texts.tooltip} + title={texts.tooltip} + > + {icon.useReactIcon ? ( + <span style={icon.reactIconStyle}> + <ReactIconByTag tag={icon.reactIconTag || "FiLock"} style={icon.reactIconStyle} /> + </span> + ) : icon.iconImage ? ( + <Image + src={icon.iconImage} + alt={texts.tooltip} + width={icon.iconImgWidth} + height={icon.iconImgHeight} + unoptimized + /> + ) : ( + "🔒" + )} + </button> + <ConfirmPopup + isOpen={isPopupOpen} + title={texts.popupTitle} + description={texts.popupDescription} + confirmText={texts.confirmText} + cancelText={texts.cancelText} + isDestructive + onConfirm={onConfirmBlock} + onCancel={() => setIsPopupOpen(false)} + /> + </> + ); +} diff --git a/frontend/src/shared/config/site-baseline.json b/frontend/src/shared/config/site-baseline.json new file mode 100644 index 0000000..329e8bd --- /dev/null +++ b/frontend/src/shared/config/site-baseline.json @@ -0,0 +1,646 @@ +{ + "name": "Git Pages Docs", + "defaultLanguage": "en", + "supportedLanguages": [ + "en", + "pt", + "es" + ], + "HideThemeSelector": false, + "ThemeDefault": "aurora-dark", + "ThemeModeDefault": "dark", + "ActiveNavigation": true, + "FocusMode": true, + "FooterEnabled": true, + "FooterLinkName": "GitPageDocs", + "FooterLinkUrl": "https://github.com/Vidigal-code/git-page-docs", + "FooterDateMode": "browser", + "FooterDateCustom": "", + "ProjectLink": "https://github.com/Vidigal-code/git-page-docs", + "SiteIconPath": "/icon.svg", + "SiteHeaderName": "Git Pages Docs", + "IconImageMenuHeaderImgWidth": 20, + "IconImageMenuHeaderImgHeight": 20, + "IconImageMenuHeaderLightImg": "", + "IconImageMenuHeaderDarkImg": "", + "IconImageMenuHeaderReactIcones": true, + "IconImageMenuHeaderReactIconesTag": "FaGithubAlt", + "IconImageMenuHeaderReactIconesTagColorDark": "White", + "IconImageMenuHeaderReactIconesTagColorLight": "black", + "IconImageMenuHeaderReactIconesTagSize": "25px", + "IconProjectLinkLightImg": "", + "IconProjectLinkDarkImg": "", + "IconProjectLinkReactIcones": true, + "IconProjectLinkReactIconesTag": "FaGithubAlt", + "IconProjectLinkReactIconesTagColorDark": "White", + "IconProjectLinkReactIconesTagColorLight": "black", + "IconProjectLinkImgWidth": 20, + "IconProjectLinkImgHeight": 20, + "IconVersionLinksLightImg": "", + "IconVersionLinksDarkImg": "", + "IconVersionLinksReactIcones": true, + "IconVersionLinksReactIconesTag": "FaCodeBranch", + "IconVersionLinksReactIconesTagColorDark": "White", + "IconVersionLinksReactIconesTagColorLight": "black", + "IconVersionLinksReactIconesTagSize": "25px", + "IconVersionLinksImgWidth": 20, + "IconVersionLinksImgHeight": 20, + "IconInfoHeaderMenuLightImg": "", + "IconInfoHeaderMenuDarkImg": "", + "IconInfoHeaderMenuReactIcones": true, + "IconInfoHeaderMenuReactIconesTag": "BsInfoSquareFill", + "IconInfoHeaderMenuReactIconesTagColorDark": "White", + "IconInfoHeaderMenuReactIconesTagColorLight": "black", + "IconInfoHeaderMenuReactIconesTagSize": "25px", + "IconInfoHeaderMenuImgWidth": 20, + "IconInfoHeaderMenuImgHeight": 20, + "IconPreviewProjectLinkLightImg": "", + "IconPreviewProjectLinkDarkImg": "", + "IconPreviewProjectLinkReactIcones": true, + "IconPreviewProjectLinkReactIconesTag": "CiGlobe", + "IconPreviewProjectLinkReactIconesTagColorDark": "White", + "IconPreviewProjectLinkReactIconesTagColorLight": "black", + "IconPreviewProjectLinkReactIconesTagSize": "25px", + "IconPreviewProjectLinkImgWidth": 20, + "IconPreviewProjectLinkImgHeight": 20, + "IconNavMenuOpenLightImg": "", + "IconNavMenuOpenDarkImg": "", + "IconNavMenuOpenReactIcones": true, + "IconNavMenuOpenReactIconesTag": "FaBars", + "IconNavMenuOpenReactIconesTagColorDark": "White", + "IconNavMenuOpenReactIconesTagColorLight": "black", + "IconNavMenuOpenReactIconesTagSize": "25px", + "IconNavMenuOpenImgWidth": 20, + "IconNavMenuOpenImgHeight": 20, + "IconNavMenuCloseLightImg": "", + "IconNavMenuCloseDarkImg": "", + "IconNavMenuCloseReactIcones": true, + "IconNavMenuCloseReactIconesTag": "IoMdClose", + "IconNavMenuCloseReactIconesTagColorDark": "White", + "IconNavMenuCloseReactIconesTagColorLight": "black", + "IconNavMenuCloseReactIconesTagSize": "25px", + "IconNavMenuCloseImgWidth": 20, + "IconNavMenuCloseImgHeight": 20, + "IconNavMenuMobileOpenLightImg": "", + "IconNavMenuMobileOpenDarkImg": "", + "IconNavMenuMobileOpenReactIcones": true, + "IconNavMenuMobileOpenReactIconesTag": "FaBars", + "IconNavMenuMobileOpenReactIconesTagColorDark": "White", + "IconNavMenuMobileOpenReactIconesTagColorLight": "black", + "IconNavMenuMobileOpenReactIconesTagSize": "25px", + "IconNavMenuMobileOpenImgWidth": 20, + "IconNavMenuMobileOpenImgHeight": 20, + "IconNavMenuMobileCloseLightImg": "", + "IconNavMenuMobileCloseDarkImg": "", + "IconNavMenuMobileCloseReactIcones": true, + "IconNavMenuMobileCloseReactIconesTag": "IoMdClose", + "IconNavMenuMobileCloseReactIconesTagColorDark": "White", + "IconNavMenuMobileCloseReactIconesTagColorLight": "black", + "IconNavMenuMobileCloseReactIconesTagSize": "25px", + "IconNavMenuMobileCloseImgWidth": 20, + "IconNavMenuMobileCloseImgHeight": 20, + "IconNavMenuBlockActiveLightImg": "", + "IconNavMenuBlockActiveDarkImg": "", + "IconNavMenuBlockActiveReactIcones": true, + "IconNavMenuBlockActiveReactIconesTag": "FiLock", + "IconNavMenuBlockActiveReactIconesTagColorDark": "White", + "IconNavMenuBlockActiveReactIconesTagColorLight": "black", + "IconNavMenuBlockActiveReactIconesTagSize": "25px", + "IconNavMenuBlockActiveImgWidth": 20, + "IconNavMenuBlockActiveImgHeight": 20, + "IconNavMenuBlockInactiveLightImg": "", + "IconNavMenuBlockInactiveDarkImg": "", + "IconNavMenuBlockInactiveReactIcones": true, + "IconNavMenuBlockInactiveReactIconesTag": "FiUnlock", + "IconNavMenuBlockInactiveReactIconesTagColorDark": "White", + "IconNavMenuBlockInactiveReactIconesTagColorLight": "black", + "IconNavMenuBlockInactiveReactIconesTagSize": "25px", + "IconNavMenuBlockInactiveImgWidth": 20, + "IconNavMenuBlockInactiveImgHeight": 20, + "IconDocsLockLightImg": "", + "IconDocsLockDarkImg": "", + "IconDocsLockReactIcones": true, + "IconDocsLockReactIconesTag": "FiLock", + "IconDocsLockReactIconesTagColorDark": "White", + "IconDocsLockReactIconesTagColorLight": "black", + "IconDocsLockReactIconesTagSize": "22px", + "IconDocsLockImgWidth": 20, + "IconDocsLockImgHeight": 20, + "RouteguideBrandPositionDefault": "center", + "RouteguideBrandContainerTopDefault": false, + "audioPlayerEnabled": true, + "audioAutoPlayOnLoad": false, + "audioLoopEnabled": false, + "audioSequentialPlayback": false, + "audioPopoverHideSource": false, + "audioPopoverShowMinutes": true, + "audioTracks": [ + { + "url": "https://www.youtube.com/watch?v=xAR6N9N8e6U", + "type": "youtube", + "title": { + "pt": "Youtube", + "en": "Youtube", + "es": "Youtube" + } + }, + { + "url": "https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh", + "type": "spotify", + "title": { + "pt": "Spotify", + "en": "Spotify", + "es": "Spotify" + } + } + ], + "IconAudioPlayReactIcones": true, + "IconAudioPlayReactIconesTag": "CiPlay1", + "IconAudioPlayReactIconesTagColorDark": "White", + "IconAudioPlayReactIconesTagColorLight": "black", + "IconAudioPlayReactIconesTagSize": "25px", + "IconAudioPauseReactIcones": true, + "IconAudioPauseReactIconesTag": "FaPause", + "IconAudioPauseReactIconesTagColorDark": "White", + "IconAudioPauseReactIconesTagColorLight": "black", + "IconAudioPauseReactIconesTagSize": "25px", + "IconAudioPlayerPopoverCloseLightImg": "", + "IconAudioPlayerPopoverCloseDarkImg": "", + "IconAudioPlayerPopoverCloseReactIcones": true, + "IconAudioPlayerPopoverCloseReactIconesTag": "IoMdClose", + "IconAudioPlayerPopoverCloseReactIconesTagColorDark": "White", + "IconAudioPlayerPopoverCloseReactIconesTagColorLight": "black", + "IconAudioPlayerPopoverCloseReactIconesTagSize": "25px", + "IconAudioPlayerPopoverCloseImgWidth": 20, + "IconAudioPlayerPopoverCloseImgHeight": 20, + "IconAudioPlayerPopoverPlayLightImg": "", + "IconAudioPlayerPopoverPlayDarkImg": "", + "IconAudioPlayerPopoverPlayReactIcones": true, + "IconAudioPlayerPopoverPlayReactIconesTag": "CiPlay1", + "IconAudioPlayerPopoverPlayReactIconesTagColorDark": "White", + "IconAudioPlayerPopoverPlayReactIconesTagColorLight": "black", + "IconAudioPlayerPopoverPlayReactIconesTagSize": "25px", + "IconAudioPlayerPopoverPlayImgWidth": 20, + "IconAudioPlayerPopoverPlayImgHeight": 20, + "IconAudioPlayerPopoverPauseLightImg": "", + "IconAudioPlayerPopoverPauseDarkImg": "", + "IconAudioPlayerPopoverPauseReactIcones": true, + "IconAudioPlayerPopoverPauseReactIconesTag": "FaPause", + "IconAudioPlayerPopoverPauseReactIconesTagColorDark": "White", + "IconAudioPlayerPopoverPauseReactIconesTagColorLight": "black", + "IconAudioPlayerPopoverPauseReactIconesTagSize": "25px", + "IconAudioPlayerPopoverPauseImgWidth": 20, + "IconAudioPlayerPopoverPauseImgHeight": 20, + "IconAudioPlayerPopoverRestartLightImg": "", + "IconAudioPlayerPopoverRestartDarkImg": "", + "IconAudioPlayerPopoverRestartReactIcones": true, + "IconAudioPlayerPopoverRestartReactIconesTag": "FiRefreshCw", + "IconAudioPlayerPopoverRestartReactIconesTagColorDark": "White", + "IconAudioPlayerPopoverRestartReactIconesTagColorLight": "black", + "IconAudioPlayerPopoverRestartReactIconesTagSize": "25px", + "IconAudioPlayerPopoverRestartImgWidth": 20, + "IconAudioPlayerPopoverRestartImgHeight": 20, + "IconAudioPlayerPopoverLoopOnLightImg": "", + "IconAudioPlayerPopoverLoopOnDarkImg": "", + "IconAudioPlayerPopoverLoopOnReactIcones": true, + "IconAudioPlayerPopoverLoopOnReactIconesTag": "FiRepeat", + "IconAudioPlayerPopoverLoopOnReactIconesTagColorDark": "White", + "IconAudioPlayerPopoverLoopOnReactIconesTagColorLight": "black", + "IconAudioPlayerPopoverLoopOnReactIconesTagSize": "25px", + "IconAudioPlayerPopoverLoopOnImgWidth": 20, + "IconAudioPlayerPopoverLoopOnImgHeight": 20, + "IconAudioPlayerPopoverLoopOffLightImg": "", + "IconAudioPlayerPopoverLoopOffDarkImg": "", + "IconAudioPlayerPopoverLoopOffReactIcones": true, + "IconAudioPlayerPopoverLoopOffReactIconesTag": "FiRepeat", + "IconAudioPlayerPopoverLoopOffReactIconesTagColorDark": "White", + "IconAudioPlayerPopoverLoopOffReactIconesTagColorLight": "black", + "IconAudioPlayerPopoverLoopOffReactIconesTagSize": "25px", + "IconAudioPlayerPopoverLoopOffImgWidth": 20, + "IconAudioPlayerPopoverLoopOffImgHeight": 20, + "IconAiChatOpenLightImg": "", + "IconAiChatOpenDarkImg": "", + "IconAiChatOpenReactIcones": true, + "IconAiChatOpenReactIconesTag": "BsChatDots", + "IconAiChatOpenReactIconesTagColorDark": "White", + "IconAiChatOpenReactIconesTagColorLight": "black", + "IconAiChatOpenReactIconesTagSize": "22px", + "IconAiChatOpenImgWidth": 20, + "IconAiChatOpenImgHeight": 20, + "IconAiChatCloseLightImg": "", + "IconAiChatCloseDarkImg": "", + "IconAiChatCloseReactIcones": true, + "IconAiChatCloseReactIconesTag": "IoMdClose", + "IconAiChatCloseReactIconesTagColorDark": "White", + "IconAiChatCloseReactIconesTagColorLight": "black", + "IconAiChatCloseReactIconesTagSize": "22px", + "IconAiChatCloseImgWidth": 20, + "IconAiChatCloseImgHeight": 20, + "IconAiChatSettingsLightImg": "", + "IconAiChatSettingsDarkImg": "", + "IconAiChatSettingsReactIcones": true, + "IconAiChatSettingsReactIconesTag": "FiSettings", + "IconAiChatSettingsReactIconesTagColorDark": "White", + "IconAiChatSettingsReactIconesTagColorLight": "black", + "IconAiChatSettingsReactIconesTagSize": "20px", + "IconAiChatSettingsImgWidth": 20, + "IconAiChatSettingsImgHeight": 20, + "IconAiChatSendLightImg": "", + "IconAiChatSendDarkImg": "", + "IconAiChatSendReactIcones": true, + "IconAiChatSendReactIconesTag": "FiSend", + "IconAiChatSendReactIconesTagColorDark": "White", + "IconAiChatSendReactIconesTagColorLight": "black", + "IconAiChatSendReactIconesTagSize": "20px", + "IconAiChatSendImgWidth": 20, + "IconAiChatSendImgHeight": 20, + "IconAiChatCancelLightImg": "", + "IconAiChatCancelDarkImg": "", + "IconAiChatCancelReactIcones": true, + "IconAiChatCancelReactIconesTag": "FiXCircle", + "IconAiChatCancelReactIconesTagColorDark": "White", + "IconAiChatCancelReactIconesTagColorLight": "black", + "IconAiChatCancelReactIconesTagSize": "20px", + "IconAiChatCancelImgWidth": 20, + "IconAiChatCancelImgHeight": 20, + "IconAiChatTrashLightImg": "", + "IconAiChatTrashDarkImg": "", + "IconAiChatTrashReactIcones": true, + "IconAiChatTrashReactIconesTag": "FiTrash2", + "IconAiChatTrashReactIconesTagColorDark": "White", + "IconAiChatTrashReactIconesTagColorLight": "black", + "IconAiChatTrashReactIconesTagSize": "20px", + "IconAiChatTrashImgWidth": 20, + "IconAiChatTrashImgHeight": 20, + "IconAiChatClearChatLightImg": "", + "IconAiChatClearChatDarkImg": "", + "IconAiChatClearChatReactIcones": true, + "IconAiChatClearChatReactIconesTag": "FiMessageSquare", + "IconAiChatClearChatReactIconesTagColorDark": "White", + "IconAiChatClearChatReactIconesTagColorLight": "black", + "IconAiChatClearChatReactIconesTagSize": "20px", + "IconAiChatClearChatImgWidth": 20, + "IconAiChatClearChatImgHeight": 20, + "IconAiChatClearDataLightImg": "", + "IconAiChatClearDataDarkImg": "", + "IconAiChatClearDataReactIcones": true, + "IconAiChatClearDataReactIconesTag": "FiDatabase", + "IconAiChatClearDataReactIconesTagColorDark": "White", + "IconAiChatClearDataReactIconesTagColorLight": "black", + "IconAiChatClearDataReactIconesTagSize": "20px", + "IconAiChatClearDataImgWidth": 20, + "IconAiChatClearDataImgHeight": 20, + "IconAiChatExpandLightImg": "", + "IconAiChatExpandDarkImg": "", + "IconAiChatExpandReactIcones": true, + "IconAiChatExpandReactIconesTag": "FiMaximize2", + "IconAiChatExpandReactIconesTagColorDark": "White", + "IconAiChatExpandReactIconesTagColorLight": "black", + "IconAiChatExpandReactIconesTagSize": "20px", + "IconAiChatExpandImgWidth": 20, + "IconAiChatExpandImgHeight": 20, + "IconAiChatCollapseLightImg": "", + "IconAiChatCollapseDarkImg": "", + "IconAiChatCollapseReactIcones": true, + "IconAiChatCollapseReactIconesTag": "FiMinimize2", + "IconAiChatCollapseReactIconesTagColorDark": "White", + "IconAiChatCollapseReactIconesTagColorLight": "black", + "IconAiChatCollapseReactIconesTagSize": "20px", + "IconAiChatCollapseImgWidth": 20, + "IconAiChatCollapseImgHeight": 20, + "IconSidebarCollapseLightImg": "", + "IconSidebarCollapseDarkImg": "", + "IconSidebarCollapseReactIcones": true, + "IconSidebarCollapseReactIconesTag": "FiChevronsLeft", + "IconSidebarCollapseReactIconesTagColorDark": "White", + "IconSidebarCollapseReactIconesTagColorLight": "black", + "IconSidebarCollapseReactIconesTagSize": "22px", + "IconSidebarCollapseImgWidth": 20, + "IconSidebarCollapseImgHeight": 20, + "IconSidebarExpandLightImg": "", + "IconSidebarExpandDarkImg": "", + "IconSidebarExpandReactIcones": true, + "IconSidebarExpandReactIconesTag": "FiChevronsRight", + "IconSidebarExpandReactIconesTagColorDark": "White", + "IconSidebarExpandReactIconesTagColorLight": "black", + "IconSidebarExpandReactIconesTagSize": "22px", + "IconSidebarExpandImgWidth": 20, + "IconSidebarExpandImgHeight": 20, + "TocScrollMaxHeightDesktop": "min(65vh, 400px)", + "TocScrollMaxHeightMobile": "min(45vh, 280px)", + "layoutsConfigPathOficial": true, + "layoutsConfigPathTemplatesOficial": "https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagedocs/layouts/templates", + "layoutsConfigPathOficialUrl": "https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagedocs/layouts/layoutsConfig.json", + "repositorySearchHome": false, + "rendering": "https://vidigal-code.github.io/git-page-docs/", + "AiChatEnabled": true, + "docsAccess": { + "enabled": false, + "publicKey": "" + }, + "langmenu": { + "pt": { + "pt": "Portugues", + "en": "Ingles", + "es": "Espanhol", + "footerLabel": "Projeto", + "menuOpen": "Menu", + "menuClose": "Fechar", + "showMenu": "Abrir menu", + "hideMenu": "Fechar menu", + "quickNavigation": "Ctrl+K", + "searchOwnerLabel": "Usuario (ex: Vidigal-code)", + "searchRepoLabel": "Repositorio (ex: git-page-link-create)", + "searchButtonLabel": "Buscar", + "typeToNavigate": "Digite para navegar...", + "noNavigationResults": "Nenhum resultado de navegacao.", + "focusMode": "Modo foco", + "versionLinksLabel": "Links do repositorio", + "titleHeaderMenuMd": "Markdown", + "titleHeaderMenuVideo": "Vídeo", + "titleHeaderMenuAudio": "Áudio", + "titleHeaderMenuHtml": "Páginas", + "lastUpdateVersionLabel": "Ultima versao de atualizacao", + "darkMode": "Modo escuro", + "lightMode": "Modo claro", + "blockMenuOnNavActive": "Bloquear menu na navegação", + "blockMenuOnNavInactive": "Permitir menu na navegação", + "audioPlayLabel": "Reproduzir música de fundo", + "audioPauseLabel": "Pausar música de fundo", + "audioPlaylistTitle": "Escolher faixa", + "audioPlaylistDescription": "Selecione uma faixa para reproduzir da playlist.", + "audioPopoverNowPlaying": "Tocando", + "audioPopoverRestart": "Reiniciar", + "audioPopoverLoopOn": "Ativar loop", + "audioPopoverLoopOff": "Desativar loop", + "audioPopoverSource": "Arquivo", + "audioPopoverStatusPlaying": "Tocando", + "audioPopoverStatusPaused": "Pausado", + "audioPopoverStatusLoopOn": "Loop ativado", + "audioPopoverStatusLoopOff": "Loop desativado", + "aiChatTitle": "Assistente de IA", + "aiChatPlaceholder": "Pergunte sobre a documentação...", + "aiChatConfigTitle": "Configurar IA", + "aiChatConfigDesc": "Insira sua chave de API para ativar o chat.", + "aiChatClearDataBtn": "Limpar dados locais", + "aiChatSendBtn": "Enviar", + "aiChatSaveStartBtn": "Salvar e iniciar conversa", + "aiChatAttachAriaLabel": "Anexar arquivo de áudio ou imagem", + "aiChatAttachedFilesLabel": "arquivo(s) anexado(s)", + "aiChatRemoveAttachmentBtn": "Remover", + "aiChatCancelResponseLabel": "Cancelar resposta", + "aiChatOpenBtnAriaLabel": "Abrir chat de IA", + "aiChatCloseBtnAriaLabel": "Fechar chat", + "aiChatSystemPrompt": "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + "aiChatEmptyStateGreeting": "Olá! Eu sou o assistente de inteligência artificial desta documentação. Você pode me fazer perguntas sobre o conteúdo ou buscar um termo em toda a documentação. Como posso ajudar você hoje?", + "aiChatDisclaimer": "A IA pode cometer erros. Sempre verifique as informações.", + "aiChatPasswordCreateDesc": "Crie uma senha local para criptografar suas chaves de API.", + "aiChatPasswordUnlockDesc": "Digite sua senha local para desbloquear.", + "aiChatPasswordPlaceholder": "Senha local", + "aiChatCreatePasswordBtn": "Criar senha", + "aiChatUnlockBtn": "Desbloquear", + "aiChatWrongPassword": "Senha incorreta.", + "aiChatLockBtn": "Bloquear", + "aiChatResetBtn": "Esqueceu a senha?", + "aiChatResetPopupTitle": "Redefinir senha?", + "aiChatResetPopupDesc": "Isso apaga todas as chaves de API salvas e a senha local. Você criará uma nova senha na próxima vez.", + "aiChatResetConfirmBtn": "Sim, redefinir", + "aiChatResetCancelBtn": "Cancelar", + "aiChatProviderLabel": "Provedor:", + "aiChatApiKeyLabel": "Chave de API (deixe em branco para IA local):", + "aiChatProviderOpenAI": "OpenAI (GPT-4o-mini)", + "aiChatProviderClaude": "Anthropic Claude (3.5 Sonnet)", + "aiChatProviderGemini": "Google Gemini (1.5 Flash)", + "aiChatProviderOllama": "Ollama Network (Local LLMs)", + "aiChatEmptyPageContent": "Nenhuma página ativa compatível.", + "aiChatNoPageId": "Sem página", + "aiChatOllamaUrlLabel": "URL da API Ollama (deixe em branco para local)", + "aiChatClearChatPopupTitle": "Limpar conversa?", + "aiChatClearChatPopupDesc": "Isso apagará todo o histórico atual do chat. Esta ação não pode ser desfeita.", + "aiChatClearChatConfirmBtn": "Sim, apagar", + "aiChatClearChatCancelBtn": "Cancelar", + "aiChatClearDataPopupTitle": "Apagar todos os dados?", + "aiChatClearDataPopupDesc": "Isso apagará todos os chats e as chaves de API locais. O chat de IA será fechado.", + "aiChatClearDataConfirmBtn": "Sim, apagar tudo", + "aiChatClearDataCancelBtn": "Cancelar", + "aiChatUserLabel": "Você", + "aiChatApiError": "Ocorreu um erro de conexão com a API.", + "aiChatError401": "Chave de acesso inválida ou provedor não autorizado.", + "aiChatError429": "Limite de requisições excedido. Tente novamente mais tarde.", + "aiChatError500": "Ocorreu um erro interno no servidor do modelo de IA.", + "aiChatErrorGeneric": "Ocorreu um erro inesperado ao se comunicar com a IA.", + "docsAccessGateTitle": "Documentação protegida", + "docsAccessGateDescription": "Digite a senha ou a chave privada para acessar a documentação.", + "docsAccessInputPlaceholder": "Senha ou chave privada", + "docsAccessUnlockBtn": "Desbloquear", + "docsAccessWrongCredential": "Senha ou chave incorreta.", + "docsAccessLockTooltip": "Bloquear documentação", + "docsAccessBlockPopupTitle": "Bloquear a documentação?", + "docsAccessBlockPopupDesc": "Você precisará da senha novamente na próxima visita.", + "docsAccessBlockConfirmBtn": "Sim, bloquear", + "docsAccessBlockCancelBtn": "Cancelar" + }, + "en": { + "pt": "Portuguese", + "en": "English", + "es": "Spanish", + "footerLabel": "Project", + "menuOpen": "Menu", + "menuClose": "Close", + "showMenu": "Show menu", + "hideMenu": "Hide menu", + "quickNavigation": "Ctrl+K", + "searchOwnerLabel": "Owner (ex: Vidigal-code)", + "searchRepoLabel": "Repository (ex: git-page-link-create)", + "searchButtonLabel": "Search", + "typeToNavigate": "Type to navigate...", + "noNavigationResults": "No navigation results.", + "focusMode": "Focus mode", + "versionLinksLabel": "Repository links", + "titleHeaderMenuMd": "Markdown", + "titleHeaderMenuVideo": "Video", + "titleHeaderMenuAudio": "Audio", + "titleHeaderMenuHtml": "Pages", + "lastUpdateVersionLabel": "Last update version", + "darkMode": "Dark mode", + "lightMode": "Light mode", + "blockMenuOnNavActive": "Block menu on navigation", + "blockMenuOnNavInactive": "Allow menu on navigation", + "audioPlayLabel": "Play background music", + "audioPauseLabel": "Pause background music", + "audioPlaylistTitle": "Choose track", + "audioPlaylistDescription": "Select a track to play from the playlist.", + "audioPopoverNowPlaying": "Now playing", + "audioPopoverRestart": "Restart", + "audioPopoverLoopOn": "Loop on", + "audioPopoverLoopOff": "Loop off", + "audioPopoverSource": "File", + "audioPopoverStatusPlaying": "Playing", + "audioPopoverStatusPaused": "Paused", + "audioPopoverStatusLoopOn": "Loop on", + "audioPopoverStatusLoopOff": "Loop off", + "aiChatTitle": "AI Assistant", + "aiChatPlaceholder": "Ask about the documentation...", + "aiChatConfigTitle": "Configure AI", + "aiChatConfigDesc": "Enter your API key to enable the chat.", + "aiChatClearDataBtn": "Clear local data", + "aiChatSendBtn": "Send", + "aiChatSaveStartBtn": "Save & Start Chatting", + "aiChatAttachAriaLabel": "Attach audio or image file", + "aiChatAttachedFilesLabel": "file(s) attached", + "aiChatRemoveAttachmentBtn": "Remove", + "aiChatCancelResponseLabel": "Cancel response", + "aiChatOpenBtnAriaLabel": "Open AI Chat", + "aiChatCloseBtnAriaLabel": "Close chat", + "aiChatSystemPrompt": "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + "aiChatEmptyStateGreeting": "Hello! I am the artificial intelligence assistant of this documentation. You can ask me questions about the content or search for a term across the documentation. How can I help you today?", + "aiChatDisclaimer": "AI can make mistakes. Always verify the information.", + "aiChatPasswordCreateDesc": "Create a local password to encrypt your API keys.", + "aiChatPasswordUnlockDesc": "Enter your local password to unlock.", + "aiChatPasswordPlaceholder": "Local password", + "aiChatCreatePasswordBtn": "Create password", + "aiChatUnlockBtn": "Unlock", + "aiChatWrongPassword": "Incorrect password.", + "aiChatLockBtn": "Lock", + "aiChatResetBtn": "Forgot password?", + "aiChatResetPopupTitle": "Reset password?", + "aiChatResetPopupDesc": "This erases all saved API keys and the local password. You'll create a new password next time.", + "aiChatResetConfirmBtn": "Yes, reset", + "aiChatResetCancelBtn": "Cancel", + "aiChatProviderLabel": "Provider:", + "aiChatApiKeyLabel": "API Key (leave blank for Local AI):", + "aiChatProviderOpenAI": "OpenAI (GPT-4o-mini)", + "aiChatProviderClaude": "Anthropic Claude (3.5 Sonnet)", + "aiChatProviderGemini": "Google Gemini (1.5 Flash)", + "aiChatProviderOllama": "Ollama Network (Local LLMs)", + "aiChatEmptyPageContent": "No compatible active page.", + "aiChatNoPageId": "No page", + "aiChatOllamaUrlLabel": "Ollama API URL (leave blank for local)", + "aiChatClearChatPopupTitle": "Clear conversation?", + "aiChatClearChatPopupDesc": "This will delete all current chat history. This action cannot be undone.", + "aiChatClearChatConfirmBtn": "Yes, delete", + "aiChatClearChatCancelBtn": "Cancel", + "aiChatClearDataPopupTitle": "Erase all data?", + "aiChatClearDataPopupDesc": "This will delete all chats and local API keys. The AI chat will be closed.", + "aiChatClearDataConfirmBtn": "Yes, erase everything", + "aiChatClearDataCancelBtn": "Cancel", + "aiChatUserLabel": "You", + "aiChatApiError": "An API connection error occurred.", + "aiChatError401": "Invalid access key or unauthorized provider.", + "aiChatError429": "Rate limit exceeded. Please try again later.", + "aiChatError500": "An internal server error occurred on the AI model.", + "aiChatErrorGeneric": "An unexpected error occurred while communicating with the AI.", + "docsAccessGateTitle": "Protected documentation", + "docsAccessGateDescription": "Enter the password or private key to view the documentation.", + "docsAccessInputPlaceholder": "Password or private key", + "docsAccessUnlockBtn": "Unlock", + "docsAccessWrongCredential": "Incorrect password or key.", + "docsAccessLockTooltip": "Lock documentation", + "docsAccessBlockPopupTitle": "Block the documentation?", + "docsAccessBlockPopupDesc": "You'll need the password again on your next visit.", + "docsAccessBlockConfirmBtn": "Yes, block", + "docsAccessBlockCancelBtn": "Cancel" + }, + "es": { + "pt": "Portugues", + "en": "Ingles", + "es": "Espanol", + "footerLabel": "Proyecto", + "menuOpen": "Menu", + "menuClose": "Cerrar", + "showMenu": "Abrir menu", + "hideMenu": "Cerrar menu", + "quickNavigation": "Ctrl+K", + "searchOwnerLabel": "Usuario (ej: Vidigal-code)", + "searchRepoLabel": "Repositorio (ej: git-page-link-create)", + "searchButtonLabel": "Buscar", + "typeToNavigate": "Escribe para navegar...", + "noNavigationResults": "Sin resultados de navegacion.", + "focusMode": "Modo foco", + "versionLinksLabel": "Enlaces del repositorio", + "titleHeaderMenuMd": "Markdown", + "titleHeaderMenuVideo": "Vídeo", + "titleHeaderMenuAudio": "Áudio", + "titleHeaderMenuHtml": "Páginas", + "lastUpdateVersionLabel": "Ultima version de actualizacion", + "darkMode": "Modo oscuro", + "lightMode": "Modo claro", + "blockMenuOnNavActive": "Bloquear menú en la navegación", + "blockMenuOnNavInactive": "Permitir menú en la navegación", + "audioPopoverNowPlaying": "Reproduciendo", + "audioPopoverRestart": "Reiniciar", + "audioPopoverLoopOn": "Activar loop", + "audioPopoverLoopOff": "Desactivar loop", + "audioPopoverSource": "Archivo", + "audioPopoverStatusPlaying": "Reproduciendo", + "audioPopoverStatusPaused": "Pausado", + "audioPopoverStatusLoopOn": "Loop activado", + "audioPopoverStatusLoopOff": "Loop desactivado", + "audioPlayLabel": "Reproducir música de fondo", + "audioPauseLabel": "Pausar música de fondo", + "audioPlaylistTitle": "Elegir pista", + "audioPlaylistDescription": "Seleccione una pista para reproducir de la playlist.", + "aiChatTitle": "Asistente de IA", + "aiChatPlaceholder": "Pregunta sobre la documentación...", + "aiChatConfigTitle": "Configurar IA", + "aiChatConfigDesc": "Introduce tu clave de API para activar el chat.", + "aiChatClearDataBtn": "Borrar datos locales", + "aiChatSendBtn": "Enviar", + "aiChatSaveStartBtn": "Guardar y empezar a chatear", + "aiChatAttachAriaLabel": "Adjuntar archivo de audio o imagen", + "aiChatAttachedFilesLabel": "archivo(s) adjunto(s)", + "aiChatRemoveAttachmentBtn": "Quitar", + "aiChatCancelResponseLabel": "Cancelar respuesta", + "aiChatOpenBtnAriaLabel": "Abrir chat de IA", + "aiChatCloseBtnAriaLabel": "Cerrar chat", + "aiChatSystemPrompt": "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + "aiChatEmptyStateGreeting": "¡Hola! Soy el asistente de inteligencia artificial de esta documentación. Puedes hacerme preguntas sobre el contenido o buscar un término en toda la documentación. ¿Cómo puedo ayudarte hoy?", + "aiChatDisclaimer": "La IA puede cometer errores. Verifica siempre la información.", + "aiChatPasswordCreateDesc": "Crea una contraseña local para cifrar tus claves de API.", + "aiChatPasswordUnlockDesc": "Introduce tu contraseña local para desbloquear.", + "aiChatPasswordPlaceholder": "Contraseña local", + "aiChatCreatePasswordBtn": "Crear contraseña", + "aiChatUnlockBtn": "Desbloquear", + "aiChatWrongPassword": "Contraseña incorrecta.", + "aiChatLockBtn": "Bloquear", + "aiChatResetBtn": "¿Olvidaste la contraseña?", + "aiChatResetPopupTitle": "¿Restablecer contraseña?", + "aiChatResetPopupDesc": "Esto borra todas las claves de API guardadas y la contraseña local. Crearás una nueva contraseña la próxima vez.", + "aiChatResetConfirmBtn": "Sí, restablecer", + "aiChatResetCancelBtn": "Cancelar", + "aiChatProviderLabel": "Proveedor:", + "aiChatApiKeyLabel": "Clave de API (déjalo en blanco para IA local):", + "aiChatProviderOpenAI": "OpenAI (GPT-4o-mini)", + "aiChatProviderClaude": "Anthropic Claude (3.5 Sonnet)", + "aiChatProviderGemini": "Google Gemini (1.5 Flash)", + "aiChatProviderOllama": "Ollama Network (Local LLMs)", + "aiChatEmptyPageContent": "Ninguna página activa compatible.", + "aiChatNoPageId": "Sin página", + "aiChatOllamaUrlLabel": "URL de la API de Ollama (déjalo en blanco para local)", + "aiChatClearChatPopupTitle": "¿Borrar conversación?", + "aiChatClearChatPopupDesc": "Esto eliminará todo el historial de chat actual. Esta acción no se puede deshacer.", + "aiChatClearChatConfirmBtn": "Sí, borrar", + "aiChatClearChatCancelBtn": "Cancelar", + "aiChatClearDataPopupTitle": "¿Borrar todos los datos?", + "aiChatClearDataPopupDesc": "Esto eliminará todos los chats y las claves de API locales. El chat de IA se cerrará.", + "aiChatClearDataConfirmBtn": "Sí, borrar todo", + "aiChatClearDataCancelBtn": "Cancelar", + "aiChatUserLabel": "Tú", + "aiChatApiError": "Se produjo un error de conexión con la API.", + "aiChatError401": "Clave de acceso no válida o proveedor no autorizado.", + "aiChatError429": "Límite de solicitudes superado. Inténtalo de nuevo más tarde.", + "aiChatError500": "Se produjo un error interno del servidor en el modelo de IA.", + "aiChatErrorGeneric": "Se produjo un error inesperado al comunicarse con la IA.", + "docsAccessGateTitle": "Documentación protegida", + "docsAccessGateDescription": "Introduce la contraseña o la clave privada para ver la documentación.", + "docsAccessInputPlaceholder": "Contraseña o clave privada", + "docsAccessUnlockBtn": "Desbloquear", + "docsAccessWrongCredential": "Contraseña o clave incorrecta.", + "docsAccessLockTooltip": "Bloquear documentación", + "docsAccessBlockPopupTitle": "¿Bloquear la documentación?", + "docsAccessBlockPopupDesc": "Tendrás que introducir la contraseña de nuevo en la próxima visita.", + "docsAccessBlockConfirmBtn": "Sí, bloquear", + "docsAccessBlockCancelBtn": "Cancelar" + } + } +} diff --git a/frontend/src/shared/config/translations-baseline.json b/frontend/src/shared/config/translations-baseline.json new file mode 100644 index 0000000..0a8e630 --- /dev/null +++ b/frontend/src/shared/config/translations-baseline.json @@ -0,0 +1,58 @@ +{ + "notFound": { + "title": { + "pt": "Pagina nao encontrada", + "en": "Page not found", + "es": "Pagina no encontrada" + }, + "description": { + "pt": "A pagina de documentacao solicitada nao existe neste contexto de repositorio.", + "en": "The requested documentation page does not exist in this repository context.", + "es": "La pagina de documentacion solicitada no existe en este contexto de repositorio." + }, + "returnHome": { + "pt": "Voltar para inicio", + "en": "Return Home", + "es": "Volver al inicio" + } + }, + "navigation": { + "previous": { + "pt": "Voltar", + "en": "Previous", + "es": "Volver" + }, + "next": { + "pt": "Proximo", + "en": "Next", + "es": "Siguiente" + }, + "menuOpen": { + "pt": "Menu", + "en": "Menu", + "es": "Menu" + }, + "menuClose": { + "pt": "Fechar", + "en": "Close", + "es": "Cerrar" + }, + "browsePrev": { + "pt": "Anterior", + "en": "Previous", + "es": "Anterior" + }, + "browseNext": { + "pt": "Proximo", + "en": "Next", + "es": "Siguiente" + } + }, + "footer": { + "footerLabel": { + "pt": "Projeto", + "en": "Project", + "es": "Proyecto" + } + } +} diff --git a/frontend/src/shared/lib/icons/ai-chat/resolve-ai-chat-icon.ts b/frontend/src/shared/lib/icons/ai-chat/resolve-ai-chat-icon.ts index 03b9636..b82c5fb 100644 --- a/frontend/src/shared/lib/icons/ai-chat/resolve-ai-chat-icon.ts +++ b/frontend/src/shared/lib/icons/ai-chat/resolve-ai-chat-icon.ts @@ -29,7 +29,7 @@ export function resolveAiChatOpenIconConfig(site: any, mode: "dark" | "light", b }; } const rawImage = mode === "dark" ? site.IconAiChatOpenDarkImg : site.IconAiChatOpenLightImg; - const useReactIcon = Boolean(site.IconAiChatOpenReactIcones); + const useReactIcon = (site.IconAiChatOpenReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatOpenReactIconesTag || "BsChatDots"; const color = mode === "dark" ? site.IconAiChatOpenReactIconesTagColorDark : site.IconAiChatOpenReactIconesTagColorLight; const size = site.IconAiChatOpenReactIconesTagSize; @@ -48,15 +48,15 @@ export function resolveAiChatCloseIconConfig(site: any, mode: "dark" | "light", return { iconImage: resolveIconPath(DEFAULT_IMG, basePath), useReactIcon: true, - reactIconTag: "IoClose", + reactIconTag: "IoMdClose", reactIconStyle: {}, iconImgWidth: 20, iconImgHeight: 20, }; } const rawImage = mode === "dark" ? site.IconAiChatCloseDarkImg : site.IconAiChatCloseLightImg; - const useReactIcon = Boolean(site.IconAiChatCloseReactIcones); - const reactIconTag = site.IconAiChatCloseReactIconesTag || "IoClose"; + const useReactIcon = (site.IconAiChatCloseReactIcones ?? !rawImage); + const reactIconTag = site.IconAiChatCloseReactIconesTag || "IoMdClose"; const color = mode === "dark" ? site.IconAiChatCloseReactIconesTagColorDark : site.IconAiChatCloseReactIconesTagColorLight; const size = site.IconAiChatCloseReactIconesTagSize; return { @@ -81,7 +81,7 @@ export function resolveAiChatSettingsIconConfig(site: any, mode: "dark" | "light }; } const rawImage = mode === "dark" ? site.IconAiChatSettingsDarkImg : site.IconAiChatSettingsLightImg; - const useReactIcon = Boolean(site.IconAiChatSettingsReactIcones); + const useReactIcon = (site.IconAiChatSettingsReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatSettingsReactIconesTag || "FiSettings"; const color = mode === "dark" ? site.IconAiChatSettingsReactIconesTagColorDark : site.IconAiChatSettingsReactIconesTagColorLight; const size = site.IconAiChatSettingsReactIconesTagSize; @@ -107,7 +107,7 @@ export function resolveAiChatSendIconConfig(site: any, mode: "dark" | "light", b }; } const rawImage = mode === "dark" ? site.IconAiChatSendDarkImg : site.IconAiChatSendLightImg; - const useReactIcon = Boolean(site.IconAiChatSendReactIcones); + const useReactIcon = (site.IconAiChatSendReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatSendReactIconesTag || "FiSend"; const color = mode === "dark" ? site.IconAiChatSendReactIconesTagColorDark : site.IconAiChatSendReactIconesTagColorLight; const size = site.IconAiChatSendReactIconesTagSize; @@ -133,7 +133,7 @@ export function resolveAiChatCancelIconConfig(site: any, mode: "dark" | "light", }; } const rawImage = mode === "dark" ? site.IconAiChatCancelDarkImg : site.IconAiChatCancelLightImg; - const useReactIcon = Boolean(site.IconAiChatCancelReactIcones); + const useReactIcon = (site.IconAiChatCancelReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatCancelReactIconesTag || "FiXCircle"; const color = mode === "dark" ? site.IconAiChatCancelReactIconesTagColorDark : site.IconAiChatCancelReactIconesTagColorLight; const size = site.IconAiChatCancelReactIconesTagSize; @@ -159,7 +159,7 @@ export function resolveAiChatTrashIconConfig(site: any, mode: "dark" | "light", }; } const rawImage = mode === "dark" ? site.IconAiChatTrashDarkImg : site.IconAiChatTrashLightImg; - const useReactIcon = Boolean(site.IconAiChatTrashReactIcones); + const useReactIcon = (site.IconAiChatTrashReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatTrashReactIconesTag || "FiTrash2"; const color = mode === "dark" ? site.IconAiChatTrashReactIconesTagColorDark : site.IconAiChatTrashReactIconesTagColorLight; const size = site.IconAiChatTrashReactIconesTagSize; @@ -185,7 +185,7 @@ export function resolveAiChatClearChatIconConfig(site: any, mode: "dark" | "ligh }; } const rawImage = mode === "dark" ? site.IconAiChatClearChatDarkImg : site.IconAiChatClearChatLightImg; - const useReactIcon = Boolean(site.IconAiChatClearChatReactIcones); + const useReactIcon = (site.IconAiChatClearChatReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatClearChatReactIconesTag || "FiMessageSquare"; const color = mode === "dark" ? site.IconAiChatClearChatReactIconesTagColorDark : site.IconAiChatClearChatReactIconesTagColorLight; const size = site.IconAiChatClearChatReactIconesTagSize; @@ -211,7 +211,7 @@ export function resolveAiChatClearDataIconConfig(site: any, mode: "dark" | "ligh }; } const rawImage = mode === "dark" ? site.IconAiChatClearDataDarkImg : site.IconAiChatClearDataLightImg; - const useReactIcon = Boolean(site.IconAiChatClearDataReactIcones); + const useReactIcon = (site.IconAiChatClearDataReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatClearDataReactIconesTag || "FiDatabase"; const color = mode === "dark" ? site.IconAiChatClearDataReactIconesTagColorDark : site.IconAiChatClearDataReactIconesTagColorLight; const size = site.IconAiChatClearDataReactIconesTagSize; @@ -237,7 +237,7 @@ export function resolveAiChatExpandIconConfig(site: any, mode: "dark" | "light", }; } const rawImage = mode === "dark" ? site.IconAiChatExpandDarkImg : site.IconAiChatExpandLightImg; - const useReactIcon = Boolean(site.IconAiChatExpandReactIcones); + const useReactIcon = (site.IconAiChatExpandReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatExpandReactIconesTag || "FiMaximize2"; const color = mode === "dark" ? site.IconAiChatExpandReactIconesTagColorDark : site.IconAiChatExpandReactIconesTagColorLight; const size = site.IconAiChatExpandReactIconesTagSize; @@ -263,7 +263,7 @@ export function resolveAiChatCollapseIconConfig(site: any, mode: "dark" | "light }; } const rawImage = mode === "dark" ? site.IconAiChatCollapseDarkImg : site.IconAiChatCollapseLightImg; - const useReactIcon = Boolean(site.IconAiChatCollapseReactIcones); + const useReactIcon = (site.IconAiChatCollapseReactIcones ?? !rawImage); const reactIconTag = site.IconAiChatCollapseReactIconesTag || "FiMinimize2"; const color = mode === "dark" ? site.IconAiChatCollapseReactIconesTagColorDark : site.IconAiChatCollapseReactIconesTagColorLight; const size = site.IconAiChatCollapseReactIconesTagSize; diff --git a/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-close-icon.ts b/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-close-icon.ts index 38ac509..c64ffb3 100644 --- a/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-close-icon.ts +++ b/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-close-icon.ts @@ -16,7 +16,7 @@ export interface AudioPlayerPopoverCloseIconConfigInput { } const DEFAULT_IMG = DEFAULT_ICON_FALLBACK_URL; -const FALLBACK_TAG = "IoClose"; +const FALLBACK_TAG = "IoMdClose"; export function resolveAudioPlayerPopoverCloseIconConfig( site: AudioPlayerPopoverCloseIconConfigInput | undefined, @@ -38,7 +38,7 @@ export function resolveAudioPlayerPopoverCloseIconConfig( ? site.IconAudioPlayerPopoverCloseDarkImg?.trim() : site.IconAudioPlayerPopoverCloseLightImg?.trim(); const iconImage = resolveIconPath(rawIconImage || DEFAULT_IMG, basePath); - const useReactIcon = Boolean(site.IconAudioPlayerPopoverCloseReactIcones); + const useReactIcon = (site.IconAudioPlayerPopoverCloseReactIcones ?? !rawIconImage); const reactIconTag = site.IconAudioPlayerPopoverCloseReactIconesTag?.trim() || FALLBACK_TAG; const reactIconColor = mode === "dark" diff --git a/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-icons.ts b/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-icons.ts index e915868..f2235c4 100644 --- a/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-icons.ts +++ b/frontend/src/shared/lib/icons/audio-popover/resolve-audio-popover-icons.ts @@ -84,7 +84,8 @@ function resolveAudioPopoverIcon( ? (s[`${prefix}DarkImg`] as string)?.trim() : (s[`${prefix}LightImg`] as string)?.trim(); const iconImage = resolveIconPath(rawIconImage || DEFAULT_IMG, basePath); - const useReactIcon = Boolean(s[`${prefix}ReactIcones`]); + const useReactIcon = + (s[`${prefix}ReactIcones`] as boolean | undefined) ?? !rawIconImage; const reactIconTag = (s[`${prefix}ReactIconesTag`] as string)?.trim() || fallbackTag; const reactIconColor = mode === "dark" diff --git a/frontend/src/shared/lib/icons/header/resolve-header-icon.ts b/frontend/src/shared/lib/icons/header/resolve-header-icon.ts index f6ebc21..9a35a22 100644 --- a/frontend/src/shared/lib/icons/header/resolve-header-icon.ts +++ b/frontend/src/shared/lib/icons/header/resolve-header-icon.ts @@ -53,8 +53,8 @@ export function resolveHeaderIconConfig( return { iconImage: resolveIconPath(undefined, basePath), headerName, - useReactIcon: false, - reactIconTag: undefined, + useReactIcon: true, + reactIconTag: "FaGithubAlt", reactIconStyle: {}, iconImgWidth: 20, iconImgHeight: 20, @@ -67,8 +67,8 @@ export function resolveHeaderIconConfig( site.IconImageMenuHeader?.trim() || site.SiteIconPath?.trim(); const iconImage = resolveIconPath(rawIconImage, basePath); - const useReactIcon = Boolean(site.IconImageMenuHeaderReactIcones); - const reactIconTag = site.IconImageMenuHeaderReactIconesTag; + const useReactIcon = (site.IconImageMenuHeaderReactIcones ?? !rawIconImage); + const reactIconTag = site.IconImageMenuHeaderReactIconesTag || "FaGithubAlt"; const reactIconColor = mode === "dark" ? site.IconImageMenuHeaderReactIconesTagColorDark diff --git a/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon-factory.ts b/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon-factory.ts index 6a842d8..91ab06a 100644 --- a/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon-factory.ts +++ b/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon-factory.ts @@ -76,6 +76,15 @@ export interface NavMenuIconConfigInput { IconSidebarExpandReactIconesTagSize?: string; IconSidebarExpandImgWidth?: string | number; IconSidebarExpandImgHeight?: string | number; + IconDocsLockLightImg?: string; + IconDocsLockDarkImg?: string; + IconDocsLockReactIcones?: boolean; + IconDocsLockReactIconesTag?: string; + IconDocsLockReactIconesTagColorDark?: string; + IconDocsLockReactIconesTagColorLight?: string; + IconDocsLockReactIconesTagSize?: string; + IconDocsLockImgWidth?: string | number; + IconDocsLockImgHeight?: string | number; } export interface ResolvedNavMenuIconConfig { @@ -127,7 +136,7 @@ const NAV_MENU_ICON_KEYS: Record<string, NavMenuIconKeysConfig> = { sizeKey: "IconNavMenuCloseReactIconesTagSize", widthKey: "IconNavMenuCloseImgWidth", heightKey: "IconNavMenuCloseImgHeight", - fallbackTag: "IoClose", + fallbackTag: "IoMdClose", }, mobileOpen: { lightKey: "IconNavMenuMobileOpenLightImg", @@ -151,7 +160,7 @@ const NAV_MENU_ICON_KEYS: Record<string, NavMenuIconKeysConfig> = { sizeKey: "IconNavMenuMobileCloseReactIconesTagSize", widthKey: "IconNavMenuMobileCloseImgWidth", heightKey: "IconNavMenuMobileCloseImgHeight", - fallbackTag: "IoClose", + fallbackTag: "IoMdClose", }, blockActive: { lightKey: "IconNavMenuBlockActiveLightImg", @@ -201,6 +210,18 @@ const NAV_MENU_ICON_KEYS: Record<string, NavMenuIconKeysConfig> = { heightKey: "IconSidebarExpandImgHeight", fallbackTag: "FiChevronsRight", }, + docsLock: { + lightKey: "IconDocsLockLightImg", + darkKey: "IconDocsLockDarkImg", + useReactKey: "IconDocsLockReactIcones", + tagKey: "IconDocsLockReactIconesTag", + colorDarkKey: "IconDocsLockReactIconesTagColorDark", + colorLightKey: "IconDocsLockReactIconesTagColorLight", + sizeKey: "IconDocsLockReactIconesTagSize", + widthKey: "IconDocsLockImgWidth", + heightKey: "IconDocsLockImgHeight", + fallbackTag: "FiLock", + }, }; function resolveIconFromKeys( @@ -224,7 +245,10 @@ function resolveIconFromKeys( ? (site[keys.darkKey] as string)?.trim() : (site[keys.lightKey] as string)?.trim(); const iconImage = resolveIconPath(rawIconImage || DEFAULT_IMG, basePath); - const useReactIcon = Boolean(site[keys.useReactKey]); + // Fallback to the default react-icon when the config provides nothing for + // this slot (no explicit flag and no custom image) — instead of a broken + // default image. An explicit flag (true/false) is always respected. + const useReactIcon = (site[keys.useReactKey] as boolean | undefined) ?? !rawIconImage; const reactIconTag = (site[keys.tagKey] as string) || keys.fallbackTag; const reactIconColor = mode === "dark" diff --git a/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon.ts b/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon.ts index d7b3c1d..d59a98d 100644 --- a/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon.ts +++ b/frontend/src/shared/lib/icons/nav-menu/resolve-nav-menu-icon.ts @@ -15,6 +15,7 @@ const resolveBlockActive = createNavMenuIconResolver("blockActive"); const resolveBlockInactive = createNavMenuIconResolver("blockInactive"); const resolveSidebarCollapse = createNavMenuIconResolver("sidebarCollapse"); const resolveSidebarExpand = createNavMenuIconResolver("sidebarExpand"); +const resolveDocsLock = createNavMenuIconResolver("docsLock"); export function resolveNavMenuOpenIconConfig( site: NavMenuIconConfigInput | undefined, @@ -85,3 +86,11 @@ export function resolveSidebarExpandIconConfig( ): ResolvedNavMenuIconConfig { return resolveSidebarExpand(site, mode, basePath); } + +export function resolveDocsLockIconConfig( + site: NavMenuIconConfigInput | undefined, + mode: "dark" | "light", + basePath: string, +): ResolvedNavMenuIconConfig { + return resolveDocsLock(site, mode, basePath); +} diff --git a/frontend/src/shared/lib/icons/route-guide/resolve-route-guide-icon.ts b/frontend/src/shared/lib/icons/route-guide/resolve-route-guide-icon.ts index 3a194d2..8235a35 100644 --- a/frontend/src/shared/lib/icons/route-guide/resolve-route-guide-icon.ts +++ b/frontend/src/shared/lib/icons/route-guide/resolve-route-guide-icon.ts @@ -44,7 +44,7 @@ export function resolveRouteGuideIconConfig( const iconImage = rawIconImage?.startsWith("http") ? rawIconImage : resolveIconPath(undefined, basePath); - const useReactIcon = Boolean(site.IconRouteGuideReactIcones); + const useReactIcon = (site.IconRouteGuideReactIcones ?? !rawIconImage); const reactIconTag = site.IconRouteGuideReactIconesTag; const reactIconColor = isDarkMode ? site.IconRouteGuideReactIconesTagColorDark diff --git a/frontend/src/widgets/ai-chat-drawer/ui/ai-chat-drawer.tsx b/frontend/src/widgets/ai-chat-drawer/ui/ai-chat-drawer.tsx index 9e25ff2..32f8dc2 100644 --- a/frontend/src/widgets/ai-chat-drawer/ui/ai-chat-drawer.tsx +++ b/frontend/src/widgets/ai-chat-drawer/ui/ai-chat-drawer.tsx @@ -307,7 +307,7 @@ export const AiChatDrawer: React.FC<AiChatDrawerProps> = ({ isOpen, onClose, ico title={labels.aiChatCloseBtnAriaLabel} className={styles.closeButton} > - {renderIcon(icons.close, "IoClose")} + {renderIcon(icons.close, "IoMdClose")} </button> </div> </div> @@ -464,8 +464,8 @@ export const AiChatDrawer: React.FC<AiChatDrawerProps> = ({ isOpen, onClose, ico <div className={styles.inputArea}> {pendingAttachments.length > 0 && ( <div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', padding: '0 0 8px 8px', display: 'flex', alignItems: 'center' }}> - {pendingAttachments.length} arquivo(s) anexado(s) - <button onClick={() => setPendingAttachments([])} style={{ marginLeft: 8, background: 'transparent', border: 'none', color: '#f87171', cursor: 'pointer' }}>Remover</button> + {pendingAttachments.length} {labels.aiChatAttachedFilesLabel} + <button onClick={() => setPendingAttachments([])} style={{ marginLeft: 8, background: 'transparent', border: 'none', color: '#f87171', cursor: 'pointer' }}>{labels.aiChatRemoveAttachmentBtn}</button> </div> )} <div className={styles.inputContainer}> @@ -497,7 +497,7 @@ export const AiChatDrawer: React.FC<AiChatDrawerProps> = ({ isOpen, onClose, ico className={styles.sendButton} data-active={inputValue.trim().length > 0 || isLoading} disabled={!inputValue.trim() && !isLoading} - title={isLoading ? "Cancelar Resposta" : labels.aiChatSendBtn} + title={isLoading ? labels.aiChatCancelResponseLabel : labels.aiChatSendBtn} > {isLoading ? renderIcon(icons.cancel, "FiXCircle") : renderIcon(icons.send, "FiSend")} </button> diff --git a/frontend/src/widgets/docs-shell/docs-shell.tsx b/frontend/src/widgets/docs-shell/docs-shell.tsx index 8221531..e362af6 100644 --- a/frontend/src/widgets/docs-shell/docs-shell.tsx +++ b/frontend/src/widgets/docs-shell/docs-shell.tsx @@ -19,6 +19,8 @@ import { resolveRouteGuideIconConfig } from "@/shared/lib/resolve-site-assets"; import { useFocusMode } from "./model/use-focus-mode"; import { useNavMenuBlockPreference } from "@/features/nav-menu-block-preference"; import { useRouteAuthorization } from "@/features/route-authorization"; +import { useDocsAccess, DocsAccessGate } from "@/features/docs-access"; +import { resolveDocsLockIconConfig } from "@/shared/lib/icons/nav-menu/resolve-nav-menu-icon"; import { useQuickNavigation } from "./model/use-quick-navigation"; import { useVersionRouting } from "./model/use-version-routing"; import { CollapsedNavRail } from "./ui/docs-shell-collapsed-rail"; @@ -359,6 +361,25 @@ export function DocsShell({ data }: { data: LoadedDocsData }) { [data.config.site, nextMode], ); + const docsAccess = useDocsAccess(data.config.site.docsAccess, data.config.site.name); + const docsLockIconConfig = useMemo( + () => resolveDocsLockIconConfig(data.config.site, nextMode === "dark" ? "dark" : "light", getBasePath()), + [data.config.site, nextMode], + ); + const docsLockProps = docsAccess.enabled + ? { + icon: docsLockIconConfig, + texts: { + tooltip: labels.docsAccessLockTooltip, + popupTitle: labels.docsAccessBlockPopupTitle, + popupDescription: labels.docsAccessBlockPopupDesc, + confirmText: labels.docsAccessBlockConfirmBtn, + cancelText: labels.docsAccessBlockCancelBtn, + }, + onConfirmBlock: docsAccess.lockAgain, + } + : undefined; + const [isAiChatOpen, setAiChatOpen] = useState(false); const aiChatIconConfig = useMemo( () => ({ @@ -417,6 +438,27 @@ export function DocsShell({ data }: { data: LoadedDocsData }) { onToggleMode, }; + if (docsAccess.state === "loading") { + return <div className={styles.wrapper} style={cssVars} />; + } + + if (docsAccess.state === "locked") { + return ( + <div className={styles.wrapper} style={cssVars}> + <DocsAccessGate + texts={{ + title: labels.docsAccessGateTitle, + description: labels.docsAccessGateDescription, + placeholder: labels.docsAccessInputPlaceholder, + unlockBtn: labels.docsAccessUnlockBtn, + wrongCredential: labels.docsAccessWrongCredential, + }} + onUnlock={docsAccess.unlock} + /> + </div> + ); + } + return ( <DocsShellProvider value={contextValue}> <div className={`${styles.wrapper} ${!sidebarOpen ? styles.wrapperCollapsed : ""}`} style={cssVars}> @@ -440,6 +482,7 @@ export function DocsShell({ data }: { data: LoadedDocsData }) { navMenuConfig={navMenuConfig} onOpenAiChat={() => setAiChatOpen(true)} aiChatIconConfig={isAiChatEnabledGlobal ? aiChatIconConfig : undefined} + docsLock={docsLockProps} /> {!sidebarOpen && ( diff --git a/frontend/src/widgets/docs-shell/model/use-build-docs-controls-config.ts b/frontend/src/widgets/docs-shell/model/use-build-docs-controls-config.ts index 9865ddf..28937f0 100644 --- a/frontend/src/widgets/docs-shell/model/use-build-docs-controls-config.ts +++ b/frontend/src/widgets/docs-shell/model/use-build-docs-controls-config.ts @@ -50,7 +50,7 @@ export function useBuildDocsControlsConfig( fallbackProjectLink: data.activeVersion?.ProjectLink?.trim() || site.ProjectLink?.trim(), projectLabel: getLangMenuLabelFromMenu(site.langmenu, language, "projectLabel", "Project"), useReactProjectLinkIcon: Boolean(site.IconProjectLinkReactIcones), - projectLinkReactIconTag: site.IconProjectLinkReactIconesTag, + projectLinkReactIconTag: site.IconProjectLinkReactIconesTag || "FaGithubAlt", projectLinkReactIconStyle: { color: (mode === "dark" ? site.IconProjectLinkReactIconesTagColorDark : site.IconProjectLinkReactIconesTagColorLight)?.trim() || undefined, fontSize: site.IconProjectLinkReactIconesTagSize?.trim() || undefined, @@ -58,7 +58,7 @@ export function useBuildDocsControlsConfig( versionLinkOptionsWithLabels, versionLinksLabel: getLangMenuLabelFromMenu(site.langmenu, language, "versionLinksLabel", "Repository links"), useReactVersionLinksIcon: Boolean(site.IconVersionLinksReactIcones), - versionLinksIconTag: site.IconVersionLinksReactIconesTag, + versionLinksIconTag: site.IconVersionLinksReactIconesTag || "FaCodeBranch", versionLinksIconStyle: { color: (mode === "dark" ? site.IconVersionLinksReactIconesTagColorDark : site.IconVersionLinksReactIconesTagColorLight)?.trim() || undefined, fontSize: site.IconVersionLinksReactIconesTagSize?.trim() || undefined, @@ -77,7 +77,7 @@ export function useBuildDocsControlsConfig( updateDate: data.activeVersion?.UpdateDate?.trim() ?? "", lastUpdateLabel: getLangMenuLabelFromMenu(site.langmenu, language, "lastUpdateVersionLabel", "Last update version"), useReactInfoIcon: Boolean(site.IconInfoHeaderMenuReactIcones), - infoIconTag: site.IconInfoHeaderMenuReactIconesTag, + infoIconTag: site.IconInfoHeaderMenuReactIconesTag || "BsInfoSquareFill", infoIconStyle: { color: (mode === "dark" ? site.IconInfoHeaderMenuReactIconesTagColorDark : site.IconInfoHeaderMenuReactIconesTagColorLight)?.trim() || undefined, fontSize: site.IconInfoHeaderMenuReactIconesTagSize?.trim() || undefined, @@ -89,7 +89,7 @@ export function useBuildDocsControlsConfig( showPreviewButton: Boolean(data.activeVersion?.PreviewProject?.trim()), previewProjectUrl: data.activeVersion?.PreviewProject?.trim() ?? "", useReactPreviewIcon: Boolean(site.IconPreviewProjectLinkReactIcones), - previewIconTag: site.IconPreviewProjectLinkReactIconesTag, + previewIconTag: site.IconPreviewProjectLinkReactIconesTag || "CiGlobe", previewIconStyle: { color: (mode === "dark" ? site.IconPreviewProjectLinkReactIconesTagColorDark : site.IconPreviewProjectLinkReactIconesTagColorLight)?.trim() || undefined, fontSize: site.IconPreviewProjectLinkReactIconesTagSize?.trim() || undefined, @@ -120,13 +120,13 @@ export function useBuildDocsControlsConfig( showAudioPlayer: Boolean(audioPlayerConfig), audioPlayerConfig, useReactAudioPlayIcon: Boolean(site.IconAudioPlayReactIcones), - audioPlayIconTag: site.IconAudioPlayReactIconesTag, + audioPlayIconTag: site.IconAudioPlayReactIconesTag || "CiPlay1", audioPlayIconStyle: { color: (mode === "dark" ? site.IconAudioPlayReactIconesTagColorDark : site.IconAudioPlayReactIconesTagColorLight)?.trim() || undefined, fontSize: site.IconAudioPlayReactIconesTagSize?.trim() || undefined, }, useReactAudioPauseIcon: Boolean(site.IconAudioPauseReactIcones), - audioPauseIconTag: site.IconAudioPauseReactIconesTag, + audioPauseIconTag: site.IconAudioPauseReactIconesTag || "FaPause", audioPauseIconStyle: { color: (mode === "dark" ? site.IconAudioPauseReactIconesTagColorDark : site.IconAudioPauseReactIconesTagColorLight)?.trim() || undefined, fontSize: site.IconAudioPauseReactIconesTagSize?.trim() || undefined, diff --git a/frontend/src/widgets/docs-shell/model/use-docs-shell-labels.ts b/frontend/src/widgets/docs-shell/model/use-docs-shell-labels.ts index 2a2f381..3010444 100644 --- a/frontend/src/widgets/docs-shell/model/use-docs-shell-labels.ts +++ b/frontend/src/widgets/docs-shell/model/use-docs-shell-labels.ts @@ -57,6 +57,25 @@ export interface DocsShellLabels { aiChatError429: string; aiChatError500: string; aiChatErrorGeneric: string; + aiChatSaveStartBtn: string; + aiChatAttachedFilesLabel: string; + aiChatRemoveAttachmentBtn: string; + aiChatCancelResponseLabel: string; + aiChatResetBtn: string; + aiChatResetPopupTitle: string; + aiChatResetPopupDesc: string; + aiChatResetConfirmBtn: string; + aiChatResetCancelBtn: string; + docsAccessGateTitle: string; + docsAccessGateDescription: string; + docsAccessInputPlaceholder: string; + docsAccessUnlockBtn: string; + docsAccessWrongCredential: string; + docsAccessLockTooltip: string; + docsAccessBlockPopupTitle: string; + docsAccessBlockPopupDesc: string; + docsAccessBlockConfirmBtn: string; + docsAccessBlockCancelBtn: string; } export function useDocsShellLabels(data: LoadedDocsData, language: string): DocsShellLabels { @@ -180,6 +199,29 @@ export function useDocsShellLabels(data: LoadedDocsData, language: string): Docs const aiChatError500 = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatError500", "An internal server error occurred on the AI model."); const aiChatErrorGeneric = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatErrorGeneric", "An unexpected error occurred while communicating with the AI."); + // Chat strings migrated from hardcoded literals + const aiChatSaveStartBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatSaveStartBtn", "Save & Start Chatting"); + const aiChatAttachedFilesLabel = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatAttachedFilesLabel", "file(s) attached"); + const aiChatRemoveAttachmentBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatRemoveAttachmentBtn", "Remove"); + const aiChatCancelResponseLabel = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatCancelResponseLabel", "Cancel response"); + const aiChatResetBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatResetBtn", "Forgot password?"); + const aiChatResetPopupTitle = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatResetPopupTitle", "Reset password?"); + const aiChatResetPopupDesc = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatResetPopupDesc", "This erases all saved API keys and the local password. You'll create a new password next time."); + const aiChatResetConfirmBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatResetConfirmBtn", "Yes, reset"); + const aiChatResetCancelBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "aiChatResetCancelBtn", "Cancel"); + + // Documentation access gate + const docsAccessGateTitle = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessGateTitle", "Protected documentation"); + const docsAccessGateDescription = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessGateDescription", "Enter the password or private key to view the documentation."); + const docsAccessInputPlaceholder = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessInputPlaceholder", "Password or private key"); + const docsAccessUnlockBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessUnlockBtn", "Unlock"); + const docsAccessWrongCredential = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessWrongCredential", "Incorrect password or key."); + const docsAccessLockTooltip = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessLockTooltip", "Lock documentation"); + const docsAccessBlockPopupTitle = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessBlockPopupTitle", "Block the documentation?"); + const docsAccessBlockPopupDesc = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessBlockPopupDesc", "You'll need the password again on your next visit."); + const docsAccessBlockConfirmBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessBlockConfirmBtn", "Yes, block"); + const docsAccessBlockCancelBtn = getLangMenuLabelFromMenu(data.config.site.langmenu, language, "docsAccessBlockCancelBtn", "Cancel"); + return { previousLabel, nextLabel, @@ -236,6 +278,25 @@ export function useDocsShellLabels(data: LoadedDocsData, language: string): Docs aiChatError429, aiChatError500, aiChatErrorGeneric, + aiChatSaveStartBtn, + aiChatAttachedFilesLabel, + aiChatRemoveAttachmentBtn, + aiChatCancelResponseLabel, + aiChatResetBtn, + aiChatResetPopupTitle, + aiChatResetPopupDesc, + aiChatResetConfirmBtn, + aiChatResetCancelBtn, + docsAccessGateTitle, + docsAccessGateDescription, + docsAccessInputPlaceholder, + docsAccessUnlockBtn, + docsAccessWrongCredential, + docsAccessLockTooltip, + docsAccessBlockPopupTitle, + docsAccessBlockPopupDesc, + docsAccessBlockConfirmBtn, + docsAccessBlockCancelBtn, }; }, [data.config.site.langmenu, data.config.translations, language]); } diff --git a/frontend/src/widgets/docs-shell/ui/docs-shell-sidebar.tsx b/frontend/src/widgets/docs-shell/ui/docs-shell-sidebar.tsx index e2998ba..e475efc 100644 --- a/frontend/src/widgets/docs-shell/ui/docs-shell-sidebar.tsx +++ b/frontend/src/widgets/docs-shell/ui/docs-shell-sidebar.tsx @@ -2,6 +2,8 @@ import Image from "next/image"; import { BsMoonStarsFill, BsSunFill } from "react-icons/bs"; import { ReactIconByTag } from "@/shared/ui/react-icon-by-tag"; import { NavMenuBlockToggle } from "@/features/nav-menu-block-preference"; +import { DocsLockButton, type DocsLockTexts } from "@/features/docs-access"; +import type { ResolvedNavMenuIconConfig } from "@/shared/lib/icons/nav-menu/resolve-nav-menu-icon"; import type { NavMenuConfig } from "../model/use-docs-shell-config"; import type { MenuNode } from "../model/menu-tree"; import { DocsShellMenuTree } from "./docs-shell-menu-tree"; @@ -27,6 +29,11 @@ interface DocsShellSidebarProps { navMenuConfig: NavMenuConfig; onOpenAiChat: () => void; aiChatIconConfig: any; + docsLock?: { + icon: ResolvedNavMenuIconConfig; + texts: DocsLockTexts; + onConfirmBlock: () => void; + }; } export function DocsShellSidebar({ @@ -49,6 +56,7 @@ export function DocsShellSidebar({ navMenuConfig, onOpenAiChat, aiChatIconConfig, + docsLock, }: DocsShellSidebarProps) { return ( <aside className={styles.sidebar}> @@ -72,6 +80,14 @@ export function DocsShellSidebar({ /> </nav> <div className={styles.sidebarFooter}> + {docsLock && ( + <DocsLockButton + icon={docsLock.icon} + texts={docsLock.texts} + onConfirmBlock={docsLock.onConfirmBlock} + className={`${styles.button} ${styles.sidebarRailButton}`} + /> + )} <NavMenuBlockToggle blockMenuOnNav={blockMenuOnNav} onToggle={() => setBlockMenuOnNav(!blockMenuOnNav)} diff --git a/gitpagedocs/config.json b/gitpagedocs/config.json index f59393d..fce7bfc 100644 --- a/gitpagedocs/config.json +++ b/gitpagedocs/config.json @@ -118,6 +118,15 @@ "IconNavMenuBlockInactiveReactIconesTagSize": "25px", "IconNavMenuBlockInactiveImgWidth": 20, "IconNavMenuBlockInactiveImgHeight": 20, + "IconDocsLockLightImg": "", + "IconDocsLockDarkImg": "", + "IconDocsLockReactIcones": true, + "IconDocsLockReactIconesTag": "FiLock", + "IconDocsLockReactIconesTagColorDark": "White", + "IconDocsLockReactIconesTagColorLight": "black", + "IconDocsLockReactIconesTagSize": "22px", + "IconDocsLockImgWidth": 20, + "IconDocsLockImgHeight": 20, "RouteguideBrandPositionDefault": "center", "RouteguideBrandContainerTopDefault": false, "audioPlayerEnabled": true, @@ -323,9 +332,13 @@ "layoutsConfigPathOficial": true, "layoutsConfigPathTemplatesOficial": "https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagedocs/layouts/templates", "layoutsConfigPathOficialUrl": "https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagedocs/layouts/layoutsConfig.json", - "repositorySearchHome": true, + "repositorySearchHome": false, "rendering": "https://vidigal-code.github.io/git-page-docs/", "AiChatEnabled": true, + "docsAccess": { + "enabled": false, + "publicKey": "" + }, "langmenu": { "pt": { "pt": "Portugues", @@ -365,7 +378,68 @@ "audioPopoverStatusPlaying": "Tocando", "audioPopoverStatusPaused": "Pausado", "audioPopoverStatusLoopOn": "Loop ativado", - "audioPopoverStatusLoopOff": "Loop desativado" + "audioPopoverStatusLoopOff": "Loop desativado", + "aiChatTitle": "Assistente de IA", + "aiChatPlaceholder": "Pergunte sobre a documentação...", + "aiChatConfigTitle": "Configurar IA", + "aiChatConfigDesc": "Insira sua chave de API para ativar o chat.", + "aiChatClearDataBtn": "Limpar dados locais", + "aiChatSendBtn": "Enviar", + "aiChatSaveStartBtn": "Salvar e iniciar conversa", + "aiChatAttachAriaLabel": "Anexar arquivo de áudio ou imagem", + "aiChatAttachedFilesLabel": "arquivo(s) anexado(s)", + "aiChatRemoveAttachmentBtn": "Remover", + "aiChatCancelResponseLabel": "Cancelar resposta", + "aiChatOpenBtnAriaLabel": "Abrir chat de IA", + "aiChatCloseBtnAriaLabel": "Fechar chat", + "aiChatSystemPrompt": "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + "aiChatEmptyStateGreeting": "Olá! Eu sou o assistente de inteligência artificial desta documentação. Você pode me fazer perguntas sobre o conteúdo ou buscar um termo em toda a documentação. Como posso ajudar você hoje?", + "aiChatDisclaimer": "A IA pode cometer erros. Sempre verifique as informações.", + "aiChatPasswordCreateDesc": "Crie uma senha local para criptografar suas chaves de API.", + "aiChatPasswordUnlockDesc": "Digite sua senha local para desbloquear.", + "aiChatPasswordPlaceholder": "Senha local", + "aiChatCreatePasswordBtn": "Criar senha", + "aiChatUnlockBtn": "Desbloquear", + "aiChatWrongPassword": "Senha incorreta.", + "aiChatLockBtn": "Bloquear", + "aiChatResetBtn": "Esqueceu a senha?", + "aiChatResetPopupTitle": "Redefinir senha?", + "aiChatResetPopupDesc": "Isso apaga todas as chaves de API salvas e a senha local. Você criará uma nova senha na próxima vez.", + "aiChatResetConfirmBtn": "Sim, redefinir", + "aiChatResetCancelBtn": "Cancelar", + "aiChatProviderLabel": "Provedor:", + "aiChatApiKeyLabel": "Chave de API (deixe em branco para IA local):", + "aiChatProviderOpenAI": "OpenAI (GPT-4o-mini)", + "aiChatProviderClaude": "Anthropic Claude (3.5 Sonnet)", + "aiChatProviderGemini": "Google Gemini (1.5 Flash)", + "aiChatProviderOllama": "Ollama Network (Local LLMs)", + "aiChatEmptyPageContent": "Nenhuma página ativa compatível.", + "aiChatNoPageId": "Sem página", + "aiChatOllamaUrlLabel": "URL da API Ollama (deixe em branco para local)", + "aiChatClearChatPopupTitle": "Limpar conversa?", + "aiChatClearChatPopupDesc": "Isso apagará todo o histórico atual do chat. Esta ação não pode ser desfeita.", + "aiChatClearChatConfirmBtn": "Sim, apagar", + "aiChatClearChatCancelBtn": "Cancelar", + "aiChatClearDataPopupTitle": "Apagar todos os dados?", + "aiChatClearDataPopupDesc": "Isso apagará todos os chats e as chaves de API locais. O chat de IA será fechado.", + "aiChatClearDataConfirmBtn": "Sim, apagar tudo", + "aiChatClearDataCancelBtn": "Cancelar", + "aiChatUserLabel": "Você", + "aiChatApiError": "Ocorreu um erro de conexão com a API.", + "aiChatError401": "Chave de acesso inválida ou provedor não autorizado.", + "aiChatError429": "Limite de requisições excedido. Tente novamente mais tarde.", + "aiChatError500": "Ocorreu um erro interno no servidor do modelo de IA.", + "aiChatErrorGeneric": "Ocorreu um erro inesperado ao se comunicar com a IA.", + "docsAccessGateTitle": "Documentação protegida", + "docsAccessGateDescription": "Digite a senha ou a chave privada para acessar a documentação.", + "docsAccessInputPlaceholder": "Senha ou chave privada", + "docsAccessUnlockBtn": "Desbloquear", + "docsAccessWrongCredential": "Senha ou chave incorreta.", + "docsAccessLockTooltip": "Bloquear documentação", + "docsAccessBlockPopupTitle": "Bloquear a documentação?", + "docsAccessBlockPopupDesc": "Você precisará da senha novamente na próxima visita.", + "docsAccessBlockConfirmBtn": "Sim, bloquear", + "docsAccessBlockCancelBtn": "Cancelar" }, "en": { "pt": "Portuguese", @@ -405,7 +479,68 @@ "audioPopoverStatusPlaying": "Playing", "audioPopoverStatusPaused": "Paused", "audioPopoverStatusLoopOn": "Loop on", - "audioPopoverStatusLoopOff": "Loop off" + "audioPopoverStatusLoopOff": "Loop off", + "aiChatTitle": "AI Assistant", + "aiChatPlaceholder": "Ask about the documentation...", + "aiChatConfigTitle": "Configure AI", + "aiChatConfigDesc": "Enter your API key to enable the chat.", + "aiChatClearDataBtn": "Clear local data", + "aiChatSendBtn": "Send", + "aiChatSaveStartBtn": "Save & Start Chatting", + "aiChatAttachAriaLabel": "Attach audio or image file", + "aiChatAttachedFilesLabel": "file(s) attached", + "aiChatRemoveAttachmentBtn": "Remove", + "aiChatCancelResponseLabel": "Cancel response", + "aiChatOpenBtnAriaLabel": "Open AI Chat", + "aiChatCloseBtnAriaLabel": "Close chat", + "aiChatSystemPrompt": "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + "aiChatEmptyStateGreeting": "Hello! I am the artificial intelligence assistant of this documentation. You can ask me questions about the content or search for a term across the documentation. How can I help you today?", + "aiChatDisclaimer": "AI can make mistakes. Always verify the information.", + "aiChatPasswordCreateDesc": "Create a local password to encrypt your API keys.", + "aiChatPasswordUnlockDesc": "Enter your local password to unlock.", + "aiChatPasswordPlaceholder": "Local password", + "aiChatCreatePasswordBtn": "Create password", + "aiChatUnlockBtn": "Unlock", + "aiChatWrongPassword": "Incorrect password.", + "aiChatLockBtn": "Lock", + "aiChatResetBtn": "Forgot password?", + "aiChatResetPopupTitle": "Reset password?", + "aiChatResetPopupDesc": "This erases all saved API keys and the local password. You'll create a new password next time.", + "aiChatResetConfirmBtn": "Yes, reset", + "aiChatResetCancelBtn": "Cancel", + "aiChatProviderLabel": "Provider:", + "aiChatApiKeyLabel": "API Key (leave blank for Local AI):", + "aiChatProviderOpenAI": "OpenAI (GPT-4o-mini)", + "aiChatProviderClaude": "Anthropic Claude (3.5 Sonnet)", + "aiChatProviderGemini": "Google Gemini (1.5 Flash)", + "aiChatProviderOllama": "Ollama Network (Local LLMs)", + "aiChatEmptyPageContent": "No compatible active page.", + "aiChatNoPageId": "No page", + "aiChatOllamaUrlLabel": "Ollama API URL (leave blank for local)", + "aiChatClearChatPopupTitle": "Clear conversation?", + "aiChatClearChatPopupDesc": "This will delete all current chat history. This action cannot be undone.", + "aiChatClearChatConfirmBtn": "Yes, delete", + "aiChatClearChatCancelBtn": "Cancel", + "aiChatClearDataPopupTitle": "Erase all data?", + "aiChatClearDataPopupDesc": "This will delete all chats and local API keys. The AI chat will be closed.", + "aiChatClearDataConfirmBtn": "Yes, erase everything", + "aiChatClearDataCancelBtn": "Cancel", + "aiChatUserLabel": "You", + "aiChatApiError": "An API connection error occurred.", + "aiChatError401": "Invalid access key or unauthorized provider.", + "aiChatError429": "Rate limit exceeded. Please try again later.", + "aiChatError500": "An internal server error occurred on the AI model.", + "aiChatErrorGeneric": "An unexpected error occurred while communicating with the AI.", + "docsAccessGateTitle": "Protected documentation", + "docsAccessGateDescription": "Enter the password or private key to view the documentation.", + "docsAccessInputPlaceholder": "Password or private key", + "docsAccessUnlockBtn": "Unlock", + "docsAccessWrongCredential": "Incorrect password or key.", + "docsAccessLockTooltip": "Lock documentation", + "docsAccessBlockPopupTitle": "Block the documentation?", + "docsAccessBlockPopupDesc": "You'll need the password again on your next visit.", + "docsAccessBlockConfirmBtn": "Yes, block", + "docsAccessBlockCancelBtn": "Cancel" }, "es": { "pt": "Portugues", @@ -445,7 +580,68 @@ "audioPlayLabel": "Reproducir música de fondo", "audioPauseLabel": "Pausar música de fondo", "audioPlaylistTitle": "Elegir pista", - "audioPlaylistDescription": "Seleccione una pista para reproducir de la playlist." + "audioPlaylistDescription": "Seleccione una pista para reproducir de la playlist.", + "aiChatTitle": "Asistente de IA", + "aiChatPlaceholder": "Pregunta sobre la documentación...", + "aiChatConfigTitle": "Configurar IA", + "aiChatConfigDesc": "Introduce tu clave de API para activar el chat.", + "aiChatClearDataBtn": "Borrar datos locales", + "aiChatSendBtn": "Enviar", + "aiChatSaveStartBtn": "Guardar y empezar a chatear", + "aiChatAttachAriaLabel": "Adjuntar archivo de audio o imagen", + "aiChatAttachedFilesLabel": "archivo(s) adjunto(s)", + "aiChatRemoveAttachmentBtn": "Quitar", + "aiChatCancelResponseLabel": "Cancelar respuesta", + "aiChatOpenBtnAriaLabel": "Abrir chat de IA", + "aiChatCloseBtnAriaLabel": "Cerrar chat", + "aiChatSystemPrompt": "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}", + "aiChatEmptyStateGreeting": "¡Hola! Soy el asistente de inteligencia artificial de esta documentación. Puedes hacerme preguntas sobre el contenido o buscar un término en toda la documentación. ¿Cómo puedo ayudarte hoy?", + "aiChatDisclaimer": "La IA puede cometer errores. Verifica siempre la información.", + "aiChatPasswordCreateDesc": "Crea una contraseña local para cifrar tus claves de API.", + "aiChatPasswordUnlockDesc": "Introduce tu contraseña local para desbloquear.", + "aiChatPasswordPlaceholder": "Contraseña local", + "aiChatCreatePasswordBtn": "Crear contraseña", + "aiChatUnlockBtn": "Desbloquear", + "aiChatWrongPassword": "Contraseña incorrecta.", + "aiChatLockBtn": "Bloquear", + "aiChatResetBtn": "¿Olvidaste la contraseña?", + "aiChatResetPopupTitle": "¿Restablecer contraseña?", + "aiChatResetPopupDesc": "Esto borra todas las claves de API guardadas y la contraseña local. Crearás una nueva contraseña la próxima vez.", + "aiChatResetConfirmBtn": "Sí, restablecer", + "aiChatResetCancelBtn": "Cancelar", + "aiChatProviderLabel": "Proveedor:", + "aiChatApiKeyLabel": "Clave de API (déjalo en blanco para IA local):", + "aiChatProviderOpenAI": "OpenAI (GPT-4o-mini)", + "aiChatProviderClaude": "Anthropic Claude (3.5 Sonnet)", + "aiChatProviderGemini": "Google Gemini (1.5 Flash)", + "aiChatProviderOllama": "Ollama Network (Local LLMs)", + "aiChatEmptyPageContent": "Ninguna página activa compatible.", + "aiChatNoPageId": "Sin página", + "aiChatOllamaUrlLabel": "URL de la API de Ollama (déjalo en blanco para local)", + "aiChatClearChatPopupTitle": "¿Borrar conversación?", + "aiChatClearChatPopupDesc": "Esto eliminará todo el historial de chat actual. Esta acción no se puede deshacer.", + "aiChatClearChatConfirmBtn": "Sí, borrar", + "aiChatClearChatCancelBtn": "Cancelar", + "aiChatClearDataPopupTitle": "¿Borrar todos los datos?", + "aiChatClearDataPopupDesc": "Esto eliminará todos los chats y las claves de API locales. El chat de IA se cerrará.", + "aiChatClearDataConfirmBtn": "Sí, borrar todo", + "aiChatClearDataCancelBtn": "Cancelar", + "aiChatUserLabel": "Tú", + "aiChatApiError": "Se produjo un error de conexión con la API.", + "aiChatError401": "Clave de acceso no válida o proveedor no autorizado.", + "aiChatError429": "Límite de solicitudes superado. Inténtalo de nuevo más tarde.", + "aiChatError500": "Se produjo un error interno del servidor en el modelo de IA.", + "aiChatErrorGeneric": "Se produjo un error inesperado al comunicarse con la IA.", + "docsAccessGateTitle": "Documentación protegida", + "docsAccessGateDescription": "Introduce la contraseña o la clave privada para ver la documentación.", + "docsAccessInputPlaceholder": "Contraseña o clave privada", + "docsAccessUnlockBtn": "Desbloquear", + "docsAccessWrongCredential": "Contraseña o clave incorrecta.", + "docsAccessLockTooltip": "Bloquear documentación", + "docsAccessBlockPopupTitle": "¿Bloquear la documentación?", + "docsAccessBlockPopupDesc": "Tendrás que introducir la contraseña de nuevo en la próxima visita.", + "docsAccessBlockConfirmBtn": "Sí, bloquear", + "docsAccessBlockCancelBtn": "Cancelar" } } }, diff --git a/gitpagedocs/docs/versions/1.0.0/en/source-viewer b/gitpagedocs/docs/versions/1.0.0/en/source-viewer index 448c336..3c28329 100644 --- a/gitpagedocs/docs/versions/1.0.0/en/source-viewer +++ b/gitpagedocs/docs/versions/1.0.0/en/source-viewer @@ -112,7 +112,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,san </div> </main> </div> -<script type="application/json" id="filesData">{"files":{"README.md":"# Git Page Docs\r\n\r\n`gitpagedocs` is a CLI and runtime contract for repository documentation.\r\n\r\nIt generates and maintains a `gitpagedocs/` folder with config and versioned markdown files. \r\nIt does **not** generate `index.html` or `index.js`.\r\n\r\n## Table of Contents\r\n\r\n- [Project Architecture (Monorepo)](#project-architecture-monorepo)\r\n- [Prerequisites](#prerequisites)\r\n- [Quick Start](#quick-start)\r\n- [Layout Strategy](#layout-strategy)\r\n- [Use Official Site or Your Own GitHub Pages](#use-official-site-or-your-own-github-pages)\r\n- [Self-Hosted GitHub Pages Setup](#self-hosted-github-pages-setup)\r\n- [Generated Structure](#generated-structure)\r\n- [Configuration Keys](#configuration-keys-layout-source)\r\n- [Version selector visibility](#version-selector-visibility)\r\n- [Repository Search Behavior](#repository-search-behavior)\r\n- [Scripts](#scripts)\r\n- [URL Routes and Query Parameters](#url-routes-and-query-parameters)\r\n- [Authorized Routes](#authorized-routes)\r\n- [CLI Options](#cli-options)\r\n- [AI CLI (interactive docs generator)](#ai-cli-interactive-docs-generator)\r\n- [Configuration File Format](#configuration-file-format)\r\n- [License](#license)\r\n\r\n## Project Architecture (Monorepo)\r\n\r\n`git-page-docs` is a **pnpm + turborepo monorepo**. All business logic lives in one shared core (`tools/`); the frontend, CLI, and MCP server are thin consumers of it.\r\n\r\n```text\r\ngit-page-docs/\r\n├── frontend/ # Next.js 15 docs viewer (static export) — see frontend/README.md\r\n├── cli/ # Hexagonal CLI, published as the `gitpagedocs` npm bin\r\n├── mcp/ # Model Context Protocol server (@gitpagedocs/mcp)\r\n├── tools/ # @gitpagedocs/tools — the ONLY home for shared business logic\r\n├── gitpagedocs/ # User contract: config + versioned docs + layouts (kept stable)\r\n├── e2e/ # Playwright end-to-end specs\r\n└── tsconfig.base.json · turbo.json · pnpm-workspace.yaml · vitest.config.ts\r\n```\r\n\r\n| Area | Package | Responsibility |\r\n| --- | --- | --- |\r\n| **frontend/** | root pkg | Next.js App Router docs viewer: multi-version / multi-language docs, 36-theme layouts, the in-docs AI chat drawer, and the `/ai` console. Built via `next build frontend` and static-exported to `out/` for GitHub Pages. |\r\n| **cli/** | `gitpagedocs` (`bin`) | Hexagonal CLI (`@clack/prompts`) that scaffolds `gitpagedocs/`, generates docs with AI, configures GitHub Pages, and launches the MCP server. |\r\n| **mcp/** | `@gitpagedocs/mcp` | MCP server (SDK 1.29): 20 tools + 7 resources for repository analysis and AI doc generation, all delegating to `tools/`. |\r\n| **tools/** | `@gitpagedocs/tools` | Shared core: 14-provider AI system (registry/factory, no switch chains), encrypted credential vault (AES-256-GCM) + password gate, logger with secret redaction, caches, config loader, filesystem + documentation services. Browser-safe subpath exports (`./ai`, `./crypto/web`, `./security/web`, …). |\r\n| **gitpagedocs/** | — | The user-facing contract: `config.{json,js,ts}`, `docs/versions/**`, `layouts/**`. Never broken by refactors. |\r\n\r\n### Security: encrypted AI credentials\r\n\r\nAPI keys are never stored in plaintext. Both the `/ai` console and the in-docs chat drawer gate access behind a **local password** that derives (PBKDF2) an AES-256-GCM key; keys are encrypted at rest in `localStorage` and decrypted only for the session. Any legacy plaintext key is migrated into the vault and wiped on first unlock.\r\n\r\n### Tooling\r\n\r\n- **pnpm workspaces** + **turborepo** for builds/tests across packages\r\n- Shared **`tsconfig.base.json`**; `npm run typecheck` covers cli / frontend / tools / mcp\r\n- **Vitest** unit + integration (coverage on `tools/src`) and **Playwright** E2E (`e2e/`)\r\n- A **smoke + byte-baseline** harness (`npm run smoke:all`) guards every legacy CLI contract\r\n- **GitHub Actions**: CI (`ci.yml`) + GitHub Pages deploy (`gitpagedocs-pages.yml`)\r\n\r\n> The sections below document the published `gitpagedocs` CLI and its runtime contract. For frontend-specific development (the Next.js viewer), see [`frontend/README.md`](frontend/README.md).\r\n\r\n## Prerequisites\r\n\r\n- **Node.js** 18+ (recommended 20+)\r\n- **npm** 9+\r\n\r\n## Quick Start\r\n\r\nInstall the CLI globally, or run it one-off:\r\n\r\n```bash\r\nnpm install -g gitpagedocs # global install\r\ngitpagedocs # then run anywhere\r\n\r\n# or, no install:\r\nnpx gitpagedocs\r\n```\r\n\r\n> `gitpagedocs` is published from the [`cli/`](cli/README.md) package of this monorepo. Generating docs is config-only — it never writes `index.html`/`index.js`.\r\n\r\nGenerate docs config and versioned files (recommended default):\r\n\r\n```bash\r\nnpx gitpagedocs\r\n```\r\n\r\nGenerate docs plus local layout templates:\r\n\r\n```bash\r\nnpx gitpagedocs --layoutconfig\r\n```\r\n\r\nGenerate docs, configure GitHub Pages URL, create workflow, and push:\r\n\r\n```bash\r\nnpx gitpagedocs --push --owner your-user --repo your-repository\r\n```\r\n\r\nDocs deploy at the repository root, e.g. `https://your-user.github.io/your-repository/v/1.0.0/?lang=en`.\r\n\r\nOptional `--path` to serve docs in a subpath (e.g. `docs` or `git-page-docs`):\r\n\r\n```bash\r\nnpx gitpagedocs --push --owner your-user --repo your-repository --path docs\r\n```\r\n\r\nThen docs are at `https://your-user.github.io/your-repository/docs/v/1.0.0/?lang=en`.\r\n\r\nShortcut syntax also supported:\r\n\r\n```bash\r\nnpx gitpagedocs --push --your-user --your-repository\r\n```\r\n\r\n### Standalone home distribution (`npx gitpagedocs --home`)\r\n\r\nGenerates a self-contained `gitpagedocshome/` folder with:\r\n\r\n- Static export of the docs (ready for `npx serve .`)\r\n- Pre-configured `.env`\r\n- `Dockerfile` for container deployment\r\n- `README.md` with usage instructions\r\n\r\n```bash\r\nnpx gitpagedocs --home\r\ncd gitpagedocshome\r\nnpx serve .\r\n```\r\n\r\nOr with Docker:\r\n\r\n```bash\r\ncd gitpagedocshome\r\ndocker build -t gitpagedocshome .\r\ndocker run -p 3000:80 gitpagedocshome\r\n```\r\n\r\n## Layout Strategy\r\n\r\n`gitpagedocs` supports two layout strategies:\r\n\r\n### 1) Default mode (`npx gitpagedocs`)\r\n\r\n- `gitpagedocs/config.json` (or `config.js` / `config.ts`) is generated with official layout source enabled.\r\n- Layouts/templates are loaded from the official repository URLs:\r\n - `https://github.com/Vidigal-code/git-page-docs/tree/main/gitpagedocs/layouts`\r\n- Best option if you want to focus only on writing docs.\r\n\r\n### 2) Local layout mode (`npx gitpagedocs --layoutconfig`)\r\n\r\n- Generates local files in `gitpagedocs/layouts/**`:\r\n - `layoutsConfig.json`\r\n - `layoutsFallbackConfig.json`\r\n - `templates/*.json`\r\n- Official layout URLs are disabled in generated config.\r\n- Best option if you want to create and maintain your own templates in your own repository.\r\n\r\n### Fallback behavior\r\n\r\n- Runtime keeps resilient fallback behavior if a layout/template source is unavailable.\r\n- In local layout mode, the runtime prioritizes local repository layout sources and does not force official template URLs by default.\r\n\r\n## Use Official Site or Your Own GitHub Pages\r\n\r\nYou can choose either:\r\n\r\n1. **Official viewer site** \r\n `https://vidigal-code.github.io/git-page-docs/`\r\n2. **Self-hosted viewer** in your own GitHub repository using GitHub Pages.\r\n\r\nThis means your docs can run independently from the official domain when you publish your own site.\r\n\r\n## Self-Hosted GitHub Pages Setup\r\n\r\n### 1) Prepare your repository\r\n\r\n```bash\r\nnpm install\r\nnpx gitpagedocs\r\n```\r\n\r\nOr, if you want local templates:\r\n\r\n```bash\r\nnpx gitpagedocs --layoutconfig\r\n```\r\n\r\n### 2) Configure runtime URL (`site.rendering`)\r\n\r\nSet `gitpagedocs/config.json` (or `config.js` / `config.ts`) `site.rendering` to your GitHub Pages URL:\r\n\r\n```text\r\nhttps://<your-user>.github.io/<your-repository>/\r\n```\r\n\r\nExample:\r\n\r\n```text\r\nhttps://octocat.github.io/my-docs/\r\n```\r\n\r\n### 3) Build and validate locally\r\n\r\n```bash\r\nnpm run lint\r\nnpm run build\r\nnpm start\r\n```\r\n\r\n### 4) Publish with GitHub Pages\r\n\r\n- Push your repository to GitHub.\r\n- Enable Pages for your repository (Settings -> Pages).\r\n- Use the repository workflow to build/deploy static output.\r\n- Optional one-command bootstrap:\r\n - `npx gitpagedocs --push --owner your-user --repo your-repository` — docs at `https://<owner>.github.io/<repo>/<repo>/v/<version>/?lang=en` (e.g. `https://vidigal-code.github.io/energy-bill-ai-parser/energy-bill-ai-parser/v/1.0.0/?lang=en`); root redirects there; base path uses repo name so CSS/JS load correctly\r\n - `npx gitpagedocs --push --owner your-user --repo your-repository --path docs` — docs at `https://<owner>.github.io/<repo>/docs/v/<version>/`\r\n - This creates `.github/workflows/gitpagedocs-pages.yml`, sets `site.rendering`, commits generated artifacts, and pushes to `origin`.\r\n - The generated workflow clones the official `git-page-docs` runtime in CI, injects your `gitpagedocs/` folder, builds, and deploys to your GitHub Pages URL.\r\n - The workflow trigger uses your current git branch automatically.\r\n - After push, CLI also attempts to switch repository Pages source to **GitHub Actions** using `gh api` (if GitHub CLI is available and authenticated).\r\n\r\nWhen built with `GITHUB_ACTIONS=true`, the runtime enables GitHub Pages behavior.\r\n\r\n## Generated Structure\r\n\r\nDefault mode:\r\n\r\n```text\r\ngitpagedocs/\r\n config.json\r\n icon.svg\r\n docs/\r\n versions/\r\n 1.0.0/config.json\r\n 1.0.0/{en,pt,es}/*.md\r\n 1.0.0/{en,pt,es}/source-viewer # Source code viewer (GitHub-style)\r\n 1.1.0/...\r\n 1.1.1/...\r\n```\r\n\r\nLocal layout mode adds:\r\n\r\n```text\r\ngitpagedocs/layouts/\r\n layoutsConfig.json\r\n layoutsFallbackConfig.json\r\n templates/*.json\r\n```\r\n\r\n## Configuration Keys (Layout Source)\r\n\r\nMain layout source keys in `gitpagedocs/config.json` (or `config.js` / `config.ts`):\r\n\r\n- `layoutsConfigPathOficial`\r\n- `layoutsConfigPathOficialUrl`\r\n- `layoutsConfigPathTemplatesOficial`\r\n- `layoutsConfigPath`\r\n- `layoutsConfigPathTemplates`\r\n\r\nBehavior:\r\n\r\n- If `layoutsConfigPathOficial=true`, runtime prefers official layout/template sources.\r\n- If `layoutsConfigPathOficial=false`, runtime prefers your repository layout/template sources (`gitpagedocs/layouts/**` or your custom paths).\r\n\r\n## Version selector visibility\r\n\r\nIn the docs shell, the **version** dropdown is hidden when `VersionControl.versions` resolves to **at most one unique** `id`:\r\n\r\n- One entry, or multiple entries with the same `id`, shows **no** version selector (duplicate `id` rows are deduplicated; the first wins).\r\n- Two or more **distinct** `id` values show the version selector.\r\n- **Language** and **theme** selectors are separate and are not affected by version count.\r\n\r\n## Repository Search Behavior\r\n\r\nRepository search is controlled by environment/runtime context:\r\n\r\n- GitHub Pages builds (`GITHUB_ACTIONS=true`): repository-search home enabled.\r\n- Local runtime: controlled by `GITPAGEDOCS_REPOSITORY_SEARCH=true|false`.\r\n\r\nRecommended for local testing:\r\n\r\n```env\r\nGITPAGEDOCS_REPOSITORY_SEARCH=true\r\n```\r\n\r\n## Scripts\r\n\r\n- `npm run gitpagedocs` — runs `node cli/index.mjs` (generate config and docs)\r\n- `npm run gitpagedocs:full` — compatibility alias for the same CLI\r\n- `npm run gitpagedocs:home` — generates `gitpagedocshome/` (static site + .env + Dockerfile + README)\r\n- `npm run build` — generate `gitpagedocs/` + copy icon to `public/` + `next build`\r\n- `npm run build:prebuilt` — generate + build + copy `out/` to `prebuilt/`\r\n- `npm run dev` — `next dev`\r\n- `npm run start` — `node cli/start.mjs` (spawns `next start`; runs after `prestart` build)\r\n- `npm run lint` — `eslint .`\r\n- `npm run clean` — remove `.next/`\r\n\r\n## URL Routes and Query Parameters\r\n\r\nAll routes for accessing documentation files on the official site or self-hosted GitHub Pages.\r\n\r\n### Path structure\r\n\r\n| Pattern | Description |\r\n|--------|-------------|\r\n| `/` | Repository search home (when `repositorySearchHome=true`) |\r\n| `/{owner}/{repo}/` | Docs for `owner/repo`, default version |\r\n| `/{owner}/{repo}/v/{version}/` | Docs for `owner/repo`, specific version |\r\n| `/v/{version}/` | Docs for the project’s own repo, specific version |\r\n\r\n**Base URL (official site):** `https://vidigal-code.github.io/git-page-docs/`\r\n\r\n### Query parameters\r\n\r\n| Parameter | Values | Description |\r\n|-----------|--------|-------------|\r\n| `lang` | `en`, `pt`, `es` | UI and content language |\r\n| `theme` | layout id (e.g. `aurora-dark`, `aurora-light`) | Active theme; always reflected in URL |\r\n| `modetheme` | `dark`, `light` | Theme mode (legacy; `theme` takes precedence) |\r\n| `version` | e.g. `1.0.0` | Version (alternative to path) |\r\n| `menu` | `en`, `pt`, `es` | Language for path resolution (use with `id` or `name`) |\r\n| `id` | route id (e.g. `1`, `2`) | Navigate to page by route id |\r\n| `name` | slug (e.g. `getting-started`) | Navigate to page by filename slug |\r\n| `mdfull` | `en`, `pt`, `es` | Markdown fullscreen mode |\r\n| `htmlfull` | `en`, `pt`, `es` | HTML fullscreen mode |\r\n| `file` | path (with `mdfull` or `htmlfull`) | File to show in fullscreen |\r\n| `videofull` | `en`, `pt`, `es` | Video fullscreen mode |\r\n| `audiofull` | `en`, `pt`, `es` | Audio fullscreen mode |\r\n| `slug` | video/audio slug (with `videofull` or `audiofull`) | Video/audio identifier |\r\n| `#heading-id` | anchor | Scroll to heading in markdown |\r\n\r\n### Example URLs (git-page-docs, English)\r\n\r\n**Markdown pages**\r\n\r\n- Getting Started (v1.0.0, aurora-dark theme): \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&theme=aurora-dark&menu=en&id=1 \r\n- Project overview: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&id=2 \r\n- GitHub issues and projects: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&id=3 \r\n- Introduction to Git: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&id=4 \r\n\r\n**By slug (`name`)**\r\n\r\n- Getting Started: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&name=getting-started \r\n- Project overview: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&name=project-overview \r\n\r\n**HTML pages** (by slug)\r\n\r\n- Source code viewer: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&name=source-viewer \r\n\r\n**Video pages** (route id 1–4; pages combine MD + HTML + Video by id)\r\n\r\n- Video 1: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?lang=en&menu=en&id=1 \r\n\r\n**Fullscreen modes**\r\n\r\n- Markdown fullscreen: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?mdfull=en&file=gitpagedocs/docs/versions/1.0.0/en/getting-started.md \r\n- HTML fullscreen: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?htmlfull=en&file=gitpagedocs/docs/versions/1.0.0/en/source-viewer \r\n- Video fullscreen: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?videofull=en&id=1 \r\n\r\n**Theme selection**\r\n\r\n- aurora-dark (default dark): \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?theme=aurora-dark \r\n- aurora-light: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.0.0/?theme=aurora-light \r\n\r\n**Other versions**\r\n\r\n- v1.1.0: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.1.0/?lang=en&theme=aurora-dark \r\n- v1.1.1: \r\n https://vidigal-code.github.io/git-page-docs/Vidigal-code/git-page-docs/v/1.1.1/?lang=en&theme=aurora-dark \r\n\r\n## Authorized Routes\r\n\r\nRoute authorization is configured per version in:\r\n\r\n- `gitpagedocs/docs/versions/<version>/config.json`\r\n\r\nUse the top-level `auth` section plus route-level `authorization`:\r\n\r\n- `auth.accessKeys`: key ids and expected secrets\r\n- `auth.rolesStorageKey`: localStorage key used to bootstrap roles\r\n- `auth.providers`: external providers (`authjs`, `clerk`, `firebase`, `jwt`)\r\n- `authorization.accessKeyId`: requires a configured key\r\n- `authorization.requiredRoles`: requires matching roles\r\n- `authorization.requireExternalAuth`: requires authenticated external provider\r\n- `authorization.allowedProviders`: optional provider allow-list per route\r\n\r\nExample:\r\n\r\n```json\r\n{\r\n \"auth\": {\r\n \"accessKeys\": {\r\n \"docs-key\": \"open-gitpagedocs-docs\"\r\n },\r\n \"providers\": [\r\n { \"type\": \"authjs\", \"enabled\": true, \"sessionEndpoint\": \"/api/auth/session\" },\r\n { \"type\": \"jwt\", \"enabled\": true, \"tokenStorageKey\": \"git-page-docs:jwt-token\" }\r\n ]\r\n },\r\n \"routes-md\": [\r\n {\r\n \"id\": 6,\r\n \"path\": {\r\n \"en\": \"gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md\",\r\n \"pt\": \"gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md\",\r\n \"es\": \"gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md\"\r\n },\r\n \"authorization\": {\r\n \"accessKeyId\": \"docs-key\",\r\n \"requiredRoles\": [\"maintainer\"],\r\n \"requireExternalAuth\": true,\r\n \"allowedProviders\": [\"authjs\", \"jwt\"]\r\n }\r\n }\r\n ]\r\n}\r\n```\r\n\r\n## CLI Options\r\n\r\n| Option | Description |\r\n|--------|-------------|\r\n| `--owner <user>` | GitHub owner (e.g. `Vidigal-code`) |\r\n| `--repo <repo>` | GitHub repository (e.g. `git-page-docs`) |\r\n| `--path <subpath>` | Subpath for docs (e.g. `docs`); without it, base path = repo name for correct asset loading on project sites |\r\n| `--output <dir>` | Output directory (default: `gitpagedocs` or `gitpagedocshome` with `--home`) |\r\n| `--search true\\|false` | Enable/disable repository search (mainly for `--home`) |\r\n| `--layoutconfig` | Generate local layout templates in `gitpagedocs/layouts/` |\r\n| `--push` | Create workflow, commit artifacts, push to origin |\r\n| `--home` | Standalone distribution in `gitpagedocshome/` (static site + .env + Dockerfile + README) |\r\n| `--interactive` / `-i` | Run in interactive mode (prompts for options) |\r\n| `ai` or `--ai` | Interactive AI documentation mode (paths, provider, API key/base URL, multilingual output) |\r\n| `--build` | Compatibility flag (no change to output) |\r\n| `--serve` | Compatibility flag |\r\n| `--full` | Compatibility flag |\r\n\r\nShortcut syntax: `npx gitpagedocs --push --<owner> --<repo>` (e.g. `--Vidigal-code --git-page-docs`) is equivalent to `--owner <owner> --repo <repo>`.\r\n\r\nWith `--home`, output is `gitpagedocshome/` (or `--output` value). Otherwise, output remains `gitpagedocs/` (or `--output` value).\r\n\r\n## AI CLI (interactive docs generator)\r\n\r\nRun:\r\n\r\n```bash\r\nnpx gitpagedocs ai\r\n```\r\n\r\nThis mode provides:\r\n\r\n- provider selection (`openai`, `claude`, `gemini`, `ollama`)\r\n- API key / base URL input\r\n- path input (supports multiple paths and cross-repo paths)\r\n- multilingual markdown generation (`pt`, `en`, `es`)\r\n- optional `.gitpagedocsconfig` persistence for manual reuse\r\n- interactive fallback when directories are missing (fix/skip/abort)\r\n\r\n### Manual config (`.gitpagedocsconfig`)\r\n\r\nYou can run manually with a persisted config in repository root:\r\n\r\n```json\r\n{\r\n \"version\": 1,\r\n \"ai\": {\r\n \"provider\": \"openai\",\r\n \"model\": \"gpt-4o-mini\",\r\n \"apiKey\": \"<YOUR_API_KEY>\",\r\n \"paths\": [\"src\", \"cli\", \"../another-repo/src\"],\r\n \"languages\": [\"pt\", \"en\", \"es\"],\r\n \"outputDir\": \"gitpagedocs/docs\",\r\n \"filePrefix\": \"ai-generated\",\r\n \"contextPrompt\": \"Você é um redator técnico sênior...\"\r\n }\r\n}\r\n```\r\n\r\nFor Ollama, use `baseUrl` instead of `apiKey`.\r\n\r\n## Configuration File Format\r\n\r\nRuntime supports three config file formats (in order of precedence):\r\n\r\n- `gitpagedocs/config.json`\r\n- `gitpagedocs/config.js` (CommonJS `module.exports` or ESM `export default`)\r\n- `gitpagedocs/config.ts` (TypeScript; exports default object)\r\n\r\n## License\r\n\r\nISC. See [repository](https://github.com/Vidigal-code/git-page-docs) for details.\r\n\r\n<!-- gitpagedocs:start -->\r\n### Supported AI providers (14)\r\n\r\n| Provider | ID | Default model | Capabilities |\r\n| --- | --- | --- | --- |\r\n| OpenAI | `openai` | `gpt-4o-mini` | stream, vision |\r\n| Anthropic | `anthropic` | `claude-sonnet-4-6` | stream, vision |\r\n| Google Gemini | `gemini` | `gemini-2.0-flash` | stream, vision, audio |\r\n| OpenRouter | `openrouter` | `openai/gpt-4o-mini` | stream, vision |\r\n| Ollama (local) | `ollama` | `llama3` | stream, vision |\r\n| Azure OpenAI | `azure-openai` | `gpt-4o-mini` | stream, vision |\r\n| Mistral | `mistral` | `mistral-large-latest` | stream |\r\n| DeepSeek | `deepseek` | `deepseek-chat` | stream |\r\n| Cohere | `cohere` | `command-r-plus` | stream |\r\n| Groq | `groq` | `llama-3.3-70b-versatile` | stream |\r\n| xAI Grok | `xai` | `grok-2-latest` | stream, vision |\r\n| Together AI | `together` | `meta-llama/Llama-3.3-70B-Instruct-Turbo` | stream |\r\n| Fireworks AI | `fireworks` | `accounts/fireworks/models/llama-v3p3-70b-instruct` | stream |\r\n| Perplexity | `perplexity` | `sonar` | stream |\r\n\r\n### CLI commands\r\n\r\n- `gitpagedocs init` — scaffold gitpagedocs config files\r\n- `gitpagedocs config` — show the resolved gitpagedocs config\r\n- `gitpagedocs provider [id]` — list AI providers or show one\r\n- `gitpagedocs models [provider]` — list catalog models\r\n- `gitpagedocs document[:repo|:file|:folder]` — generate documentation with AI\r\n- `gitpagedocs deploy | pages` — configure GitHub Pages via Actions and push\r\n- `gitpagedocs doctor` — diagnose the environment\r\n- `gitpagedocs mcp start` — start the MCP server over stdio\r\n- `gitpagedocs version` — print the CLI version\r\n- `gitpagedocs update` — show how to update the CLI\r\n<!-- gitpagedocs:end -->\r\n","package.json":"{\r\n \"name\": \"gitpagedocs-monorepo\",\r\n \"version\": \"1.1.44\",\r\n \"private\": true,\r\n \"description\": \"Workspace root for Git Page Docs — frontend (Next.js viewer), cli (published `gitpagedocs`), mcp, and tools (shared core).\",\r\n \"packageManager\": \"pnpm@10.33.2\",\r\n \"engines\": {\r\n \"node\": \">=20\"\r\n },\r\n \"workspaces\": [\r\n \"frontend\",\r\n \"cli\",\r\n \"tools\",\r\n \"mcp\"\r\n ],\r\n \"scripts\": {\r\n \"predev\": \"node -e \\\"const fs=require('fs'); const s='gitpagedocs/icon.svg'; const d='frontend/public/icon.svg'; if(fs.existsSync(s)){fs.mkdirSync('frontend/public',{recursive:true}); fs.copyFileSync(s,d);}\\\"\",\r\n \"dev\": \"cross-env GITPAGEDOCS_REPOSITORY_SEARCH=false next dev frontend\",\r\n \"clean\": \"node -e \\\"const fs=require('fs'); try{fs.rmSync('frontend/.next',{recursive:true})}catch(e){}\\\"\",\r\n \"build\": \"node cli/index.mjs && node -e \\\"const fs=require('fs'); const src='gitpagedocs/icon.svg'; const dest='frontend/public/icon.svg'; if(fs.existsSync(src)){fs.mkdirSync('frontend/public',{recursive:true}); fs.copyFileSync(src,dest);}\\\" && next build frontend\",\r\n \"build:prebuilt\": \"node cli/index.mjs && node -e \\\"const fs=require('fs'); const src='gitpagedocs/icon.svg'; const dest='frontend/public/icon.svg'; if(fs.existsSync(src)){fs.mkdirSync('frontend/public',{recursive:true}); fs.copyFileSync(src,dest);}\\\" && next build frontend && node -e \\\"const fs=require('fs'); if(fs.existsSync('cli/prebuilt')) fs.rmSync('cli/prebuilt',{recursive:true}); fs.cpSync('frontend/out','cli/prebuilt',{recursive:true}); fs.writeFileSync('cli/prebuilt/.nojekyll','')\\\"\",\r\n \"prestart\": \"npm run build\",\r\n \"start\": \"cross-env GITPAGEDOCS_REPOSITORY_SEARCH=false node cli/start.mjs\",\r\n \"lint\": \"eslint .\",\r\n \"lint:src\": \"eslint \\\"frontend/src/**/*.{ts,tsx}\\\"\",\r\n \"typecheck\": \"tsc --noEmit && tsc --noEmit -p frontend/tsconfig.json && tsc --noEmit -p tools/tsconfig.json && tsc --noEmit -p mcp/tsconfig.json\",\r\n \"test:unit\": \"vitest run\",\r\n \"test:cov\": \"vitest run --coverage\",\r\n \"test:e2e\": \"playwright test\",\r\n \"test:ci\": \"npm run smoke:cli && npm run smoke:flags && npm run smoke:core && npm run smoke:ai && npm run smoke:secweb && npm run smoke:mcp && npm run smoke:docs\",\r\n \"typecheck:strict-unused\": \"tsc --noEmit --noUnusedLocals --noUnusedParameters\",\r\n \"smoke:cli\": \"node tools/smoke/cli-smoke.mjs\",\r\n \"smoke:flags\": \"node --import tsx tools/smoke/flag-contract.ts\",\r\n \"smoke:core\": \"node --import tsx tools/smoke/core-selftest.ts\",\r\n \"smoke:ai\": \"node --import tsx tools/smoke/ai-selftest.ts\",\r\n \"smoke:secweb\": \"node --import tsx tools/smoke/security-web-selftest.ts\",\r\n \"smoke:mcp\": \"node --import tsx mcp/smoke/mcp-selftest.ts\",\r\n \"smoke:docs\": \"node --import tsx tools/smoke/docs-selftest.ts\",\r\n \"smoke:all\": \"npm run smoke:cli && npm run smoke:flags && npm run smoke:core && npm run smoke:ai && npm run smoke:secweb && npm run smoke:mcp && npm run smoke:docs && npm run baseline:check\",\r\n \"test\": \"npm run smoke:all\",\r\n \"baseline:create\": \"node tools/smoke/create-baseline.mjs\",\r\n \"baseline:check\": \"node tools/smoke/check-baseline.mjs\",\r\n \"turbo:build\": \"turbo run build\",\r\n \"turbo:lint\": \"turbo run lint\",\r\n \"turbo:typecheck\": \"turbo run typecheck\",\r\n \"turbo:test\": \"turbo run test\",\r\n \"gitpagedocs\": \"node cli/index.mjs\",\r\n \"gitpagedocs:home\": \"node cli/index.mjs --home\"\r\n },\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"git+https://github.com/Vidigal-code/git-page-docs.git\"\r\n },\r\n \"author\": \"Vidigal-code\",\r\n \"license\": \"ISC\",\r\n \"bugs\": {\r\n \"url\": \"https://github.com/Vidigal-code/git-page-docs/issues\"\r\n },\r\n \"homepage\": \"https://vidigal-code.github.io/git-page-docs/\",\r\n \"devDependencies\": {\r\n \"@eslint/eslintrc\": \"^3.3.5\",\r\n \"@gitpagedocs/mcp\": \"workspace:*\",\r\n \"@gitpagedocs/tools\": \"workspace:*\",\r\n \"@playwright/test\": \"^1.60.0\",\r\n \"@types/node\": \"^22.13.4\",\r\n \"@types/react\": \"^19.0.8\",\r\n \"@types/react-dom\": \"^19.0.3\",\r\n \"@vitest/coverage-v8\": \"^4.1.8\",\r\n \"cross-env\": \"^7.0.3\",\r\n \"eslint\": \"^9.20.1\",\r\n \"eslint-config-next\": \"^15.1.7\",\r\n \"gray-matter\": \"^4.0.3\",\r\n \"marked\": \"^15.0.7\",\r\n \"next\": \"^15.1.7\",\r\n \"react\": \"^19.0.0\",\r\n \"react-dom\": \"^19.0.0\",\r\n \"react-icons\": \"^5.4.0\",\r\n \"tsx\": \"^4.21.0\",\r\n \"turbo\": \"^2.9.18\",\r\n \"typescript\": \"5.9.3\",\r\n \"vitest\": \"^4.1.8\"\r\n }\r\n}\r\n","tsconfig.json":"{\r\n \"extends\": \"./tsconfig.base.json\",\r\n \"compilerOptions\": {\r\n \"lib\": [\"ES2022\"],\r\n \"types\": [\"node\"],\r\n \"noEmit\": true\r\n },\r\n \"include\": [\"cli/**/*.ts\"],\r\n \"exclude\": [\"node_modules\", \"frontend\", \"tools\", \"mcp\"]\r\n}\r\n","cli/ai/application/ai-command.ts":"/**\r\n * @file ai-command.ts\r\n * @description Application service for running AI commands. Decoupled from concrete LLMs.\r\n */\r\nimport { ILLMProvider } from '../core/ports/illm-provider';\r\n\r\nexport class AiCommandService {\r\n constructor(private provider: ILLMProvider) { }\r\n\r\n /**\r\n * Executes the AI Documentation Generation for a specific content\r\n */\r\n async runGeneration(content: string, contextPrompt?: string): Promise<string> {\r\n return await this.provider.generateDocumentation(content, contextPrompt);\r\n }\r\n\r\n /**\r\n * Executes a CLI chat prompt\r\n */\r\n async runChat(prompt: string): Promise<string> {\r\n const messages = [{ role: 'user' as const, content: prompt }];\r\n return await this.provider.chat(messages);\r\n }\r\n}\r\n","cli/ai/application/ai-provider-factory.ts":"import type { AiProviderId } from \"../config\";\r\nimport type { ILLMProvider } from \"../core/ports/illm-provider\";\r\nimport { ToolsLlmProvider } from \"../infrastructure/llm/tools-llm-provider\";\r\n\r\n/**\r\n * Creates the CLI LLM provider. Backed by the shared @gitpagedocs/tools AI core\r\n * (one registry-driven implementation for every provider) — no per-provider\r\n * switch. The legacy provider id (openai/claude/gemini/ollama) is mapped to the\r\n * catalog inside ToolsLlmProvider.\r\n */\r\nexport function createAiProvider(input: {\r\n provider: AiProviderId;\r\n model: string;\r\n apiKey?: string;\r\n baseUrl?: string;\r\n}): ILLMProvider {\r\n return new ToolsLlmProvider(input.provider, {\r\n model: input.model,\r\n apiKey: input.apiKey,\r\n baseUrl: input.baseUrl,\r\n });\r\n}\r\n","cli/ai/application/run-ai-cli-command.ts":"import path from \"node:path\";\r\nimport { AiCommandService } from \"./ai-command\";\r\nimport { createAiProvider } from \"./ai-provider-factory\";\r\nimport { FileSystemAdapter, type FilePayload } from \"../infrastructure/file-system-adapter\";\r\nimport {\r\n AiConfigFileRepository,\r\n} from \"../infrastructure/ai-config-file\";\r\nimport type { AiCliRunPlan, AiCliRunSummary } from \"../core/models/ai-cli-config\";\r\nimport { promptMissingDirectories, runAiInteractivePrompt } from \"../presentation/ai-prompts\";\r\n\r\nconst LANGUAGE_TO_LABEL: Record<string, string> = {\r\n pt: \"Portuguese\",\r\n en: \"English\",\r\n es: \"Español\",\r\n};\r\n\r\nfunction aggregateFilesPayload(files: FilePayload[]): string {\r\n return files\r\n .map((file) => `\\n\\n--- FILE: ${file.filePath} ---\\n\\n${file.content}`)\r\n .join(\"\");\r\n}\r\n\r\nfunction buildLanguagePrompt(basePrompt: string, language: \"pt\" | \"en\" | \"es\"): string {\r\n const languageLabel = LANGUAGE_TO_LABEL[language] ?? language;\r\n return `${basePrompt}\\n\\nMandatory output rules:\\n- Respond 100% in ${languageLabel}.\\n- Generate professional markdown for technical documentation.\\n- Include architectural overview, layer responsibilities, and next steps.\\n- Avoid content outside the scope of the analyzed files.`;\r\n}\r\n\r\nasync function collectFilesWithInteractiveFallback(\r\n fileSystem: FileSystemAdapter,\r\n initialPaths: string[],\r\n): Promise<{ files: FilePayload[]; scanned: string[]; skipped: string[] }> {\r\n let queue = [...initialPaths];\r\n let scanned: string[] = [];\r\n let skipped: string[] = [];\r\n const files: FilePayload[] = [];\r\n\r\n while (queue.length > 0) {\r\n const { existing, missing } = fileSystem.splitExistingDirectories(queue);\r\n scanned = [...scanned, ...existing];\r\n\r\n for (const directory of existing) {\r\n const directoryFiles = await fileSystem.readDirectoryRecursively(directory);\r\n files.push(...directoryFiles);\r\n }\r\n\r\n if (!missing.length) break;\r\n\r\n const fallback = await promptMissingDirectories(missing);\r\n if (fallback.abort) {\r\n skipped = [...skipped, ...missing];\r\n break;\r\n }\r\n\r\n if (!fallback.replacementPaths.length) {\r\n skipped = [...skipped, ...missing];\r\n break;\r\n }\r\n\r\n queue = fallback.replacementPaths;\r\n }\r\n\r\n return {\r\n files,\r\n scanned,\r\n skipped,\r\n };\r\n}\r\n\r\nasync function runPlan(plan: AiCliRunPlan): Promise<AiCliRunSummary> {\r\n const provider = createAiProvider({\r\n provider: plan.config.ai.provider,\r\n model: plan.config.ai.model,\r\n apiKey: plan.config.ai.apiKey,\r\n baseUrl: plan.config.ai.baseUrl,\r\n });\r\n\r\n const aiService = new AiCommandService(provider);\r\n const fileSystem = new FileSystemAdapter();\r\n\r\n const { files, scanned, skipped } = await collectFilesWithInteractiveFallback(\r\n fileSystem,\r\n plan.config.ai.paths,\r\n );\r\n\r\n if (!files.length) {\r\n return {\r\n scannedDirectories: scanned,\r\n skippedDirectories: skipped,\r\n scannedFilesCount: 0,\r\n outputs: [],\r\n };\r\n }\r\n\r\n const payload = aggregateFilesPayload(files);\r\n const outputs: string[] = [];\r\n\r\n for (const language of plan.config.ai.languages) {\r\n const prompt = buildLanguagePrompt(plan.config.ai.contextPrompt, language);\r\n const markdown = await aiService.runGeneration(payload, prompt);\r\n const outputFile = path.posix.join(\r\n plan.config.ai.outputDir.replace(/\\\\/g, \"/\"),\r\n `${plan.config.ai.filePrefix}-${plan.config.ai.provider}-${language}.md`,\r\n );\r\n await fileSystem.writeDocumentationOutput(markdown, outputFile);\r\n outputs.push(outputFile);\r\n }\r\n\r\n return {\r\n scannedDirectories: scanned,\r\n skippedDirectories: skipped,\r\n scannedFilesCount: files.length,\r\n outputs,\r\n };\r\n}\r\n\r\nexport async function runAiCliCommand(options: {\r\n cwd: string;\r\n onInfo?: (message: string) => void;\r\n}): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> {\r\n const logInfo = options.onInfo ?? (() => undefined);\r\n const configRepo = new AiConfigFileRepository(options.cwd);\r\n\r\n const existingConfig = await configRepo.read();\r\n const plan = await runAiInteractivePrompt(existingConfig);\r\n\r\n if (plan.saveConfig) {\r\n await configRepo.write(plan.config);\r\n logInfo(`[gitpagedocs:ai] Configuration saved to ${configRepo.getConfigPath()}`);\r\n }\r\n\r\n const summary = await runPlan(plan);\r\n return {\r\n summary,\r\n runConfigScaffold: plan.runConfigScaffold,\r\n };\r\n}\r\n","cli/ai/config.ts":"/**\r\n * @file config.ts\r\n * @description Provider constants for the AI CLI. Co-located in cli/ so the\r\n * published `gitpagedocs` package does not reach into the frontend app. The\r\n * browser keeps its own copy at frontend/src/shared/config/ai-config.ts.\r\n */\r\n\r\n/** Legacy 4-provider id used by the interactive CLI (mapped to the 14-provider\r\n * catalog in @gitpagedocs/tools via legacyProviderToCatalogId). */\r\nexport type AiProviderId = 'openai' | 'claude' | 'gemini' | 'ollama';\r\n\r\nexport const AI_MODEL_DEFAULTS: Readonly<Record<AiProviderId, string>> = {\r\n openai: 'gpt-4o-mini',\r\n claude: 'claude-3-5-sonnet-20240620',\r\n gemini: 'gemini-1.5-flash',\r\n ollama: 'llama3',\r\n};\r\n\r\nexport const DEFAULT_AI_DOC_PROMPT =\r\n 'You are a senior tech writer. Describe the provided code.';\r\n\r\nexport const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434';\r\n","cli/ai/core/models/ai-cli-config.ts":"import type { AiProviderId } from \"../../config\";\r\n\r\nexport const AI_CLI_CONFIG_FILENAME = \".gitpagedocsconfig\";\r\n\r\nexport interface AiCliConfig {\r\n version: 1;\r\n ai: {\r\n provider: AiProviderId;\r\n model: string;\r\n apiKey?: string;\r\n baseUrl?: string;\r\n paths: string[];\r\n languages: Array<\"pt\" | \"en\" | \"es\">;\r\n outputDir: string;\r\n filePrefix: string;\r\n contextPrompt: string;\r\n };\r\n}\r\n\r\nexport interface AiCliRunPlan {\r\n config: AiCliConfig;\r\n saveConfig: boolean;\r\n runConfigScaffold: boolean;\r\n}\r\n\r\nexport interface AiCliRunSummary {\r\n scannedDirectories: string[];\r\n skippedDirectories: string[];\r\n scannedFilesCount: number;\r\n outputs: string[];\r\n}\r\n","cli/ai/core/ports/illm-provider.ts":"/**\r\n * @file illm-provider.ts\r\n * @description Contract for all LLM implementations ensuring OCP and DIP.\r\n * Isolates the AI logic from the specific LLM providers (OpenAI, Claude, Ollama, etc.).\r\n */\r\n\r\nexport interface ChatMessage {\r\n role: 'user' | 'assistant' | 'system';\r\n content: string;\r\n type?: 'text' | 'image' | 'audio'; // Supporting multimedia as requested\r\n}\r\n\r\nexport interface ILLMProviderConfig {\r\n apiKey?: string;\r\n baseUrl?: string;\r\n model?: string;\r\n}\r\n\r\nexport interface ILLMProvider {\r\n /**\r\n * Generates markdown documentation based on file content.\r\n * @param fileContent Raw content of the source file\r\n * @param contextPrompt Optional instructions to guide the generation (e.g., FSD standard)\r\n */\r\n generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string>;\r\n\r\n /**\r\n * Conversational chat support.\r\n * @param messages Array of chat messages (system context, user history, etc.)\r\n */\r\n chat(messages: ChatMessage[]): Promise<string>;\r\n}\r\n","cli/ai/infrastructure/ai-config-file.ts":"import fs from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport {\r\n AI_CLI_CONFIG_FILENAME,\r\n type AiCliConfig,\r\n} from \"../core/models/ai-cli-config\";\r\n\r\nexport class AiConfigFileRepository {\r\n constructor(private readonly cwd: string = process.cwd()) {}\r\n\r\n getConfigPath(): string {\r\n return path.join(this.cwd, AI_CLI_CONFIG_FILENAME);\r\n }\r\n\r\n async read(): Promise<AiCliConfig | null> {\r\n const configPath = this.getConfigPath();\r\n try {\r\n const content = await fs.readFile(configPath, \"utf-8\");\r\n const parsed = JSON.parse(content) as Partial<AiCliConfig>;\r\n if (!parsed || parsed.version !== 1 || !parsed.ai) return null;\r\n return parsed as AiCliConfig;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n async write(config: AiCliConfig): Promise<void> {\r\n const configPath = this.getConfigPath();\r\n const data = JSON.stringify(config, null, 2);\r\n await fs.writeFile(configPath, `${data}\\n`, \"utf-8\");\r\n }\r\n}\r\n","cli/ai/infrastructure/file-system-adapter.ts":"import fs from 'node:fs/promises';\r\nimport path from 'node:path';\r\nimport { statSync } from 'node:fs';\r\n\r\nexport interface FilePayload {\r\n filePath: string;\r\n content: string;\r\n}\r\n\r\nexport class FileSystemAdapter {\r\n splitExistingDirectories(targetDirectories: string[]): { existing: string[]; missing: string[] } {\r\n const existing: string[] = [];\r\n const missing: string[] = [];\r\n\r\n for (const targetDirectory of targetDirectories) {\r\n const absolutePath = path.resolve(process.cwd(), targetDirectory);\r\n try {\r\n const stat = statSync(absolutePath);\r\n if (stat.isDirectory()) {\r\n existing.push(targetDirectory);\r\n } else {\r\n missing.push(targetDirectory);\r\n }\r\n } catch {\r\n missing.push(targetDirectory);\r\n }\r\n }\r\n\r\n return { existing, missing };\r\n }\r\n\r\n async readDirectoryRecursively(targetDirectory: string): Promise<FilePayload[]> {\r\n const absolutePath = path.resolve(process.cwd(), targetDirectory);\r\n const resolvedFiles: FilePayload[] = [];\r\n\r\n try {\r\n await this.traverse(absolutePath, resolvedFiles);\r\n } catch (error) {\r\n }\r\n\r\n return resolvedFiles;\r\n }\r\n\r\n private async traverse(currentPath: string, payloadList: FilePayload[]): Promise<void> {\r\n let stats;\r\n try {\r\n stats = await fs.stat(currentPath);\r\n } catch {\r\n return;\r\n }\r\n\r\n if (stats.isDirectory()) {\r\n const basename = path.basename(currentPath);\r\n if (['node_modules', '.git', '.next', 'out', 'dist'].includes(basename)) {\r\n return;\r\n }\r\n\r\n const entries = await fs.readdir(currentPath);\r\n for (const entry of entries) {\r\n await this.traverse(path.join(currentPath, entry), payloadList);\r\n }\r\n } else if (stats.isFile()) {\r\n const ext = path.extname(currentPath);\r\n const validExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.css'];\r\n if (validExtensions.includes(ext)) {\r\n try {\r\n const content = await fs.readFile(currentPath, 'utf-8');\r\n payloadList.push({\r\n filePath: currentPath,\r\n content: content\r\n });\r\n } catch {\r\n }\r\n }\r\n }\r\n }\r\n\r\n async writeDocumentationOutput(outputContent: string, targetPath: string = \"documentation-output.md\"): Promise<void> {\r\n const absoluteDest = path.resolve(process.cwd(), targetPath);\r\n await fs.mkdir(path.dirname(absoluteDest), { recursive: true });\r\n await fs.writeFile(absoluteDest, outputContent, 'utf-8');\r\n }\r\n}\r\n","cli/ai/infrastructure/llm/tools-llm-provider.ts":"import { createDefaultFactory, legacyProviderToCatalogId } from \"@gitpagedocs/tools/ai\";\r\nimport type { AiMessage, ProviderConfig } from \"@gitpagedocs/tools/ports\";\r\nimport type { ILLMProvider, ChatMessage, ILLMProviderConfig } from \"../../core/ports/illm-provider\";\r\nimport { DEFAULT_AI_DOC_PROMPT, type AiProviderId } from \"../../config\";\r\n\r\nconst factory = createDefaultFactory();\r\n\r\n/**\r\n * Single CLI LLM provider backed by the shared @gitpagedocs/tools AI core.\r\n * Replaces the four duplicated CLI provider classes (OpenAI/Claude/Gemini/\r\n * Ollama) with one adapter over the registry/factory, preserving the\r\n * ILLMProvider contract used by the AI documentation flow.\r\n */\r\nexport class ToolsLlmProvider implements ILLMProvider {\r\n private readonly providerId;\r\n private readonly model: string;\r\n private readonly apiKey?: string;\r\n private readonly baseUrl?: string;\r\n\r\n constructor(legacyProvider: AiProviderId, config: ILLMProviderConfig) {\r\n this.providerId = legacyProviderToCatalogId(legacyProvider);\r\n this.model = config.model ?? \"\";\r\n this.apiKey = config.apiKey;\r\n this.baseUrl = config.baseUrl;\r\n }\r\n\r\n async generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string> {\r\n return this.chat([\r\n { role: \"system\", content: contextPrompt || DEFAULT_AI_DOC_PROMPT },\r\n { role: \"user\", content: fileContent },\r\n ]);\r\n }\r\n\r\n async chat(messages: ChatMessage[]): Promise<string> {\r\n const system = messages\r\n .filter((m) => m.role === \"system\")\r\n .map((m) => m.content)\r\n .join(\"\\n\");\r\n const aiMessages: AiMessage[] = messages\r\n .filter((m) => m.role !== \"system\")\r\n .map((m) => ({ role: m.role as AiMessage[\"role\"], content: m.content }));\r\n\r\n const config: ProviderConfig = {\r\n providerId: this.providerId,\r\n model: this.model,\r\n apiKey: this.apiKey,\r\n baseUrl: this.baseUrl,\r\n };\r\n const response = await factory\r\n .create(this.providerId)\r\n .generate({ messages: aiMessages, system: system || undefined, maxTokens: 4000 }, config);\r\n return response.text;\r\n }\r\n}\r\n","cli/ai/presentation/ai-prompts.ts":"import {\r\n AI_MODEL_DEFAULTS,\r\n OLLAMA_DEFAULT_BASE_URL,\r\n type AiProviderId,\r\n} from '../config';\r\nimport { PROVIDER_CATALOG, legacyProviderToCatalogId } from '@gitpagedocs/tools/ai';\r\nimport type { AiCliConfig, AiCliRunPlan } from '../core/models/ai-cli-config';\r\nimport {\r\n askText,\r\n askConfirm,\r\n askSelect,\r\n askMultiSelect,\r\n intro,\r\n} from '../../presentation/ui/clack';\r\n\r\nexport interface InteractiveCliAnswers {\r\n provider: AiProviderId;\r\n apiKeyOrHost: string;\r\n model: string;\r\n targetDirs: string[];\r\n languages: Array<'pt' | 'en' | 'es'>;\r\n outputDir: string;\r\n filePrefix: string;\r\n saveConfig: boolean;\r\n runConfigScaffold: boolean;\r\n contextPrompt: string;\r\n}\r\n\r\nfunction parseCommaSeparated(value: string): string[] {\r\n return value\r\n .split(',')\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nfunction validateCommaSeparatedDirectories(value: string): string | undefined {\r\n const values = parseCommaSeparated(value);\r\n if (!values.length) return 'Provide at least one path.';\r\n return undefined;\r\n}\r\n\r\nconst CUSTOM_MODEL = '__custom_model__';\r\n\r\n/**\r\n * Ask for the model as a SELECT of the provider's real catalog models (with the\r\n * default marked), plus a \"Custom\" escape hatch — so an invalid id like \"4.8\"\r\n * can't be typed by mistake and rejected later as a 404 by the provider.\r\n */\r\nasync function askModel(provider: AiProviderId): Promise<string> {\r\n const catalogId = legacyProviderToCatalogId(provider);\r\n const spec = catalogId ? PROVIDER_CATALOG[catalogId] : undefined;\r\n const fallback = AI_MODEL_DEFAULTS[provider] || '';\r\n\r\n if (!spec || spec.models.length === 0) {\r\n return String(await askText({ message: 'Which model do you want to use?', defaultValue: fallback }));\r\n }\r\n\r\n const picked = await askSelect<string>(\r\n 'Which model do you want to use?',\r\n [\r\n ...spec.models.map((m) => ({\r\n value: m.id,\r\n label: m.id === spec.defaultModel ? `${m.id} (default)` : m.id,\r\n })),\r\n { value: CUSTOM_MODEL, label: 'Custom — enter a model id manually' },\r\n ],\r\n spec.defaultModel,\r\n );\r\n\r\n if (picked !== CUSTOM_MODEL) return picked;\r\n\r\n return String(\r\n await askText({\r\n message: 'Enter the model id:',\r\n defaultValue: spec.defaultModel,\r\n validate: (value) => (value.trim() ? undefined : 'Provide a model id'),\r\n }),\r\n );\r\n}\r\n\r\nfunction toPlan(answers: InteractiveCliAnswers): AiCliRunPlan {\r\n const config: AiCliConfig = {\r\n version: 1,\r\n ai: {\r\n provider: answers.provider,\r\n model: answers.model,\r\n apiKey: answers.provider === 'ollama' ? undefined : answers.apiKeyOrHost,\r\n baseUrl: answers.provider === 'ollama' ? answers.apiKeyOrHost : undefined,\r\n paths: answers.targetDirs,\r\n languages: answers.languages,\r\n outputDir: answers.outputDir,\r\n filePrefix: answers.filePrefix,\r\n contextPrompt: answers.contextPrompt,\r\n },\r\n };\r\n\r\n return {\r\n config,\r\n saveConfig: answers.saveConfig,\r\n runConfigScaffold: answers.runConfigScaffold,\r\n };\r\n}\r\n\r\nexport async function promptMissingDirectories(\r\n missingDirectories: string[],\r\n): Promise<{ replacementPaths: string[]; abort: boolean }> {\r\n const recoveryMode = await askSelect(\r\n `I couldn't find ${missingDirectories.length} directory(ies): ${missingDirectories.join(', ')}`,\r\n [\r\n { value: 'fix', label: 'Fix paths now (recommended)' },\r\n { value: 'skip', label: 'Skip missing paths and continue' },\r\n { value: 'abort', label: 'Abort execution' },\r\n ],\r\n 'fix',\r\n );\r\n\r\n if (recoveryMode === 'abort') {\r\n return { replacementPaths: [], abort: true };\r\n }\r\n if (recoveryMode === 'skip') {\r\n return { replacementPaths: [], abort: false };\r\n }\r\n\r\n const fixedPaths = await askText({\r\n message: 'Enter new paths (comma-separated):',\r\n validate: validateCommaSeparatedDirectories,\r\n });\r\n\r\n return {\r\n replacementPaths: parseCommaSeparated(String(fixedPaths || '')),\r\n abort: false,\r\n };\r\n}\r\n\r\nexport async function promptUseExistingConfig(existingConfig: AiCliConfig): Promise<AiCliRunPlan | null> {\r\n const useExistingConfig = await askConfirm(\r\n 'Found .gitpagedocsconfig. Do you want to use this configuration?',\r\n true,\r\n );\r\n\r\n if (!useExistingConfig) return null;\r\n\r\n const runConfigScaffold = await askConfirm(\r\n 'After generating docs with AI, run the default gitpagedocs scaffolding?',\r\n true,\r\n );\r\n\r\n return {\r\n config: existingConfig,\r\n saveConfig: false,\r\n runConfigScaffold,\r\n };\r\n}\r\n\r\nexport async function runAiInteractivePrompt(existingConfig?: AiCliConfig | null): Promise<AiCliRunPlan> {\r\n intro('🤖 GitPageDocs AI CLI — interactive mode 🤖');\r\n\r\n if (existingConfig) {\r\n const existingPlan = await promptUseExistingConfig(existingConfig);\r\n if (existingPlan) return existingPlan;\r\n }\r\n\r\n const provider = await askSelect<AiProviderId>(\r\n 'Which AI provider should be used to generate documentation?',\r\n [\r\n { value: 'openai', label: 'OpenAI (GPT)' },\r\n { value: 'claude', label: 'Anthropic (Claude)' },\r\n { value: 'gemini', label: 'Google Gemini' },\r\n { value: 'ollama', label: 'Ollama (Local Open Source)' },\r\n ],\r\n 'openai',\r\n );\r\n\r\n const apiKeyOrHost = await askText({\r\n message: provider === 'ollama' ? 'Ollama host URL:' : 'Provider API key:',\r\n defaultValue: provider === 'ollama' ? OLLAMA_DEFAULT_BASE_URL : undefined,\r\n validate: (input) => (input.length > 0 ? undefined : 'Required field'),\r\n });\r\n\r\n const model = await askModel(provider);\r\n\r\n const targetDirsRaw = await askText({\r\n message:\r\n 'Paths to analyze (comma-separated). You can include multiple repositories (e.g., src,cli,../other-repo/src):',\r\n defaultValue: 'src,cli',\r\n validate: validateCommaSeparatedDirectories,\r\n });\r\n\r\n const languages = await askMultiSelect<'en' | 'pt' | 'es'>(\r\n 'Documentation languages to generate:',\r\n [\r\n { value: 'en', label: 'English (en)' },\r\n { value: 'pt', label: 'Portuguese (pt)' },\r\n { value: 'es', label: 'Español (es)' },\r\n ],\r\n ['en', 'pt', 'es'],\r\n );\r\n\r\n const outputDir = await askText({\r\n message: 'Output directory for generated markdown files:',\r\n defaultValue: 'gitpagedocs/docs',\r\n validate: (value) => (value.trim() ? undefined : 'Provide an output directory'),\r\n });\r\n\r\n const filePrefix = await askText({\r\n message: 'Output file prefix:',\r\n defaultValue: 'ai-generated',\r\n validate: (value) => (value.trim() ? undefined : 'Provide a prefix'),\r\n });\r\n\r\n const contextPrompt = await askText({\r\n message: 'Extra instructions for AI (optional):',\r\n defaultValue:\r\n 'You are a senior technical writer. Generate professional, clear documentation with architectural insight following FSD, SOLID, and Clean Code principles.',\r\n });\r\n\r\n const saveConfig = await askConfirm(\r\n 'Save this configuration to .gitpagedocsconfig for future manual use?',\r\n true,\r\n );\r\n\r\n const runConfigScaffold = await askConfirm(\r\n 'After generating docs with AI, run the default gitpagedocs scaffolding?',\r\n true,\r\n );\r\n\r\n return toPlan({\r\n provider,\r\n apiKeyOrHost,\r\n model,\r\n targetDirs: parseCommaSeparated(targetDirsRaw),\r\n languages,\r\n outputDir,\r\n filePrefix,\r\n contextPrompt,\r\n saveConfig,\r\n runConfigScaffold,\r\n });\r\n}\r\n","cli/application/config-only/handler.mjs":"/** Config-only use case - orchestrates build, write, and optional push */\n\nimport { sanitizeSegment } from \"../../domain/services/sanitize-segment.mjs\";\nimport { reportAll } from \"../report/config-only-reporter.mjs\";\n\n/**\n * Execute config-only use case\n * @param {Object} params\n * @param {Object} params.options - Parsed CLI options\n * @param {string} params.root - Project root\n * @param {string} params.pkgRoot - Package root\n * @param {string} params.prebuiltDir - Prebuilt directory path\n * @param {Function} params.buildConfigArtifacts - Artifact builder\n * @param {Function} params.createThemeTemplate - Theme template creator\n * @param {Array} params.layouts - Layout definitions\n * @param {import(\"../ports/cli-runtime-ports\").ConfigOnlyRuntimePort} runtime\n */\nexport async function executeConfigOnly(params, runtime) {\n if (!runtime) {\n throw new Error(\"Config-only runtime is required.\");\n }\n\n const { options, root, pkgRoot, prebuiltDir, buildConfigArtifacts, createThemeTemplate, layouts } = params;\n\n const artifacts = buildConfigArtifacts({\n useLocalLayoutConfig: options.useLocalLayoutConfig,\n githubOwner: sanitizeSegment(options.githubOwner),\n githubRepo: sanitizeSegment(options.githubRepo),\n root,\n });\n\n await runtime.writeConfigOnlyOutput({\n root,\n pkgRoot,\n outputDir: options.outputDir,\n artifacts,\n useLocalLayoutConfig: options.useLocalLayoutConfig,\n layouts,\n createThemeTemplate,\n });\n\n if (options.shouldPush) {\n await runtime.ensureGitHubPagesWorkflow(\n () => runtime.getCurrentGitBranch(root),\n (relativePath, content) => runtime.writeText(root, relativePath, content),\n options.docsPath || \"\",\n );\n runtime.runGitPushForGeneratedArtifacts(options, root, sanitizeSegment);\n }\n\n const reportLines = reportAll(options, runtime.hasPath(prebuiltDir));\n for (const line of reportLines) {\n runtime.logInfo(line);\n }\n}\n","cli/application/home/handler.mjs":"/** Home distribution use case - static site + auxiliary files */\n\nimport { STATIC_OUTPUT_DIR, ARTIFACTS_DIR } from \"../../home/constants.mjs\";\n\n/**\n * Execute home distribution use case\n * @param {Object} params\n * @param {Object} params.options - Parsed CLI options\n * @param {string} params.root - Project root\n * @param {string} params.pkgRoot - Package root\n * @param {Function} params.buildConfigArtifacts - Artifact builder\n * @param {Function} params.createThemeTemplate - Theme template creator\n * @param {Array} params.layouts - Layout definitions\n * @param {import(\"../ports/cli-runtime-ports\").HomeRuntimePort} runtime\n */\nexport async function executeHome(params, runtime) {\n if (!runtime) {\n throw new Error(\"Home runtime is required.\");\n }\n\n const { options, root, pkgRoot, buildConfigArtifacts, createThemeTemplate, layouts } = params;\n const outputDir = options.outputDir || \"gitpagedocshome\";\n const repositorySearch = options.repositorySearch ?? false;\n const basePath = options.basePath ?? \"\";\n\n const isCurrentDir = outputDir === \".\" || outputDir === \"./\";\n const targetDir = isCurrentDir ? root : runtime.joinPath(root, outputDir);\n\n if (!isCurrentDir) {\n runtime.ensureDirEmpty(root, outputDir);\n }\n\n const pathSegment = basePath ? basePath.replace(/^\\/+|\\/+$/g, \"\") : \"\";\n const buildEnv = {\n ...process.env,\n GITHUB_ACTIONS: \"true\",\n GITPAGEDOCS_REPOSITORY_SEARCH: String(repositorySearch),\n GITPAGEDOCS_BASE_PATH: pathSegment ? `/${pathSegment}` : \"/\",\n GITPAGEDOCS_PATH: pathSegment,\n };\n\n const artifacts = buildConfigArtifacts({\n useLocalLayoutConfig: false,\n githubOwner: \"\",\n githubRepo: \"\",\n root,\n });\n\n await runtime.writeConfigOnlyOutput({\n root,\n pkgRoot,\n outputDir: ARTIFACTS_DIR,\n artifacts,\n useLocalLayoutConfig: false,\n layouts,\n createThemeTemplate,\n });\n\n runtime.runNextBuild(root, buildEnv);\n\n const outPath = runtime.joinPath(root, STATIC_OUTPUT_DIR);\n if (!runtime.existsPath(outPath)) {\n throw new Error(`Static export not found at ${STATIC_OUTPUT_DIR}/. Ensure next.config exports when GITHUB_ACTIONS=true.`);\n }\n\n const destBase = isCurrentDir ? root : targetDir;\n if (isCurrentDir) {\n for (const name of runtime.readDirNames(outPath)) {\n runtime.copyRecursive(runtime.joinPath(outPath, name), runtime.joinPath(destBase, name));\n }\n } else {\n runtime.copyRecursive(outPath, destBase);\n }\n runtime.copyRecursive(runtime.joinPath(root, ARTIFACTS_DIR), runtime.joinPath(destBase, ARTIFACTS_DIR));\n\n await runtime.writeHomeFiles(root, isCurrentDir ? \".\" : outputDir, { repositorySearch, basePath });\n await runtime.writeText(root, (isCurrentDir ? \"\" : outputDir + \"/\") + \".nojekyll\", \"\");\n\n const cdDir = isCurrentDir ? \".\" : outputDir;\n runtime.logSuccess(`Generated: ${cdDir}/ (static site + .env + Dockerfile + README.md)`);\n runtime.logInfo(`Serve: cd ${cdDir} && npx serve .`);\n runtime.logInfo(`Docker: cd ${cdDir} && docker build -t gitpagedocshome . && docker run -p 3000:80 gitpagedocshome`);\n}\n","cli/application/ports/cli-runtime-ports.ts":"import type { CliOptions } from \"../../domain/models/cli-options\";\r\n\r\nexport interface BuildArtifactsInput {\r\n useLocalLayoutConfig: boolean;\r\n githubOwner: string;\r\n githubRepo: string;\r\n root: string;\r\n}\r\n\r\nexport interface LayoutDefinition {\r\n file: string;\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface BuiltConfigArtifacts {\r\n rootConfig: unknown;\r\n layoutsConfig: unknown;\r\n fallbackLayoutsConfig: unknown;\r\n docs?: Record<string, Record<string, string>>;\r\n docsHtml?: Record<string, Record<string, string>>;\r\n versionConfigs: Record<string, Record<string, unknown>>;\r\n}\r\n\r\nexport interface CliRuntimeParams {\r\n root: string;\r\n pkgRoot: string;\r\n prebuiltDir: string;\r\n buildConfigArtifacts: (input: BuildArtifactsInput) => BuiltConfigArtifacts;\r\n createThemeTemplate: (layout: LayoutDefinition) => unknown;\r\n layouts: LayoutDefinition[];\r\n}\r\n\r\nexport interface CliCommandContext extends CliRuntimeParams {\r\n options: CliOptions;\r\n}\r\n\r\nexport interface CliCommandRunner {\r\n runConfigOnly: (params: CliCommandContext) => Promise<void>;\r\n runHome: (params: CliCommandContext) => Promise<void>;\r\n runAi: (params: CliCommandContext) => Promise<void>;\r\n}\r\n\r\nexport interface ConfigOnlyOutputInput {\r\n root: string;\r\n pkgRoot: string;\r\n outputDir: string;\r\n artifacts: BuiltConfigArtifacts;\r\n useLocalLayoutConfig: boolean;\r\n layouts: LayoutDefinition[];\r\n createThemeTemplate: (layout: LayoutDefinition) => unknown;\r\n}\r\n\r\nexport interface ConfigOnlyRuntimePort {\r\n hasPath: (absolutePath: string) => boolean;\r\n writeConfigOnlyOutput: (input: ConfigOnlyOutputInput) => Promise<void>;\r\n writeText: (root: string, relativePath: string, data: string) => Promise<void>;\r\n ensureGitHubPagesWorkflow: (\r\n getCurrentGitBranch: () => string,\r\n writeText: (relativePath: string, content: string) => Promise<void>,\r\n docsPath?: string,\r\n ) => Promise<void>;\r\n getCurrentGitBranch: (root: string) => string;\r\n runGitPushForGeneratedArtifacts: (\r\n options: CliOptions,\r\n root: string,\r\n sanitizeSegment: (value: string | undefined | null) => string,\r\n ) => void;\r\n logInfo: (message: string) => void;\r\n}\r\n\r\nexport interface HomeRuntimePort {\r\n joinPath: (...parts: string[]) => string;\r\n ensureDirEmpty: (root: string, relativeDir: string) => void;\r\n copyRecursive: (src: string, dest: string) => void;\r\n readDirNames: (absolutePath: string) => string[];\r\n existsPath: (absolutePath: string) => boolean;\r\n runNextBuild: (root: string, env: NodeJS.ProcessEnv) => void;\r\n writeConfigOnlyOutput: (input: ConfigOnlyOutputInput) => Promise<void>;\r\n writeHomeFiles: (\r\n root: string,\r\n outputDir: string,\r\n options: { repositorySearch?: boolean; basePath?: string },\r\n ) => Promise<void>;\r\n writeText: (root: string, relativePath: string, data: string) => Promise<void>;\r\n logSuccess: (message: string) => void;\r\n logInfo: (message: string) => void;\r\n}\r\n","cli/application/report/config-only-reporter.mjs":"/** Config-only command output messages - SRP: single place for all report messages */\n\nexport function reportConfigOnlySuccess(options) {\n return [`Generated: ${options.outputDir}/ (config-only)`, \"No index.html/index.js generated.\"];\n}\n\nexport function reportLayoutConfig(options) {\n if (options.useLocalLayoutConfig) {\n return [\"Local layouts generated in gitpagedocs/layouts/ (--layoutconfig).\"];\n } else {\n return [\"Using official remote layouts config by default (no local gitpagedocs/layouts generated).\"];\n }\n}\n\nexport function reportRenderingUrl(options) {\n if (options.githubOwner && options.githubRepo) {\n return [\n `Configured rendering URL: https://${options.githubOwner}.github.io/${options.githubRepo}/`,\n `Official viewer remains available: https://vidigal-code.github.io/git-page-docs/${options.githubOwner}/${options.githubRepo}?modetheme=light&lang=pt`,\n ];\n }\n return [];\n}\n\nexport function reportPushMode() {\n return [\n \"Generated: .github/workflows/gitpagedocs-pages.yml\",\n \"Push mode enabled: committed and pushed gitpagedocs/ + workflow to origin.\",\n ];\n}\n\nexport function reportBuildFlag() {\n return [\"`--build` keeps compatibility flag but output remains gitpagedocs/.\"];\n}\n\nexport function reportFullOrServeSkipped() {\n return [\"External commands were skipped (no prebuilt copy and no local serve spawn).\"];\n}\n\nexport function reportPrebuiltDetected() {\n return [\"`prebuilt/` detected, but ignored by config-only generator.\"];\n}\n\nexport function reportAll(options, prebuiltDetected) {\n const lines = [];\n lines.push(...reportConfigOnlySuccess(options));\n lines.push(...reportLayoutConfig(options));\n lines.push(...reportRenderingUrl(options));\n if (options.shouldPush) {\n lines.push(...reportPushMode());\n }\n if (options.isBuild) {\n lines.push(...reportBuildFlag());\n }\n if (options.mode === \"full\" || options.isServe) {\n lines.push(...reportFullOrServeSkipped());\n }\n if (prebuiltDetected) {\n lines.push(...reportPrebuiltDetected());\n }\n return lines;\n}\n","cli/application/use-cases/dispatch-mode.ts":"import type { CliOptions } from \"../../domain/models/cli-options\";\r\nimport type { CliCommandRunner, CliRuntimeParams } from \"../ports/cli-runtime-ports\";\r\n\r\nexport async function dispatchMode(\r\n options: CliOptions,\r\n params: CliRuntimeParams,\r\n runner: CliCommandRunner,\r\n): Promise<void> {\r\n if (options.mode === \"ai\") {\r\n await runner.runAi({ ...params, options });\r\n return;\r\n }\r\n if (options.mode === \"home\") {\r\n await runner.runHome({ ...params, options });\r\n return;\r\n }\r\n await runner.runConfigOnly({ ...params, options });\r\n}\r\n","cli/builders/config-orchestrator.mjs":"/** Orchestrate build of config artifacts for gitpagedocs */\n\nimport { LAYOUTS, FALLBACK_LAYOUTS } from \"../data/layouts.mjs\";\nimport { DOCS } from \"../content/docs.mjs\";\nimport { DOC_VERSIONS } from \"../data/version-constants.mjs\";\nimport { buildRootConfig } from \"./root-config-builder.mjs\";\nimport { buildVersionConfig } from \"./version-config-builder.mjs\";\nimport { generateSourceViewerHtml } from \"./source-viewer.mjs\";\n\n/**\n * Build all config artifacts (root, layouts, versions, docs).\n * @param {object} options - { useLocalLayoutConfig, githubOwner, githubRepo, root }\n * @returns {object} { rootConfig, layoutsConfig, fallbackLayoutsConfig, docs, docsHtml, versionConfigs }\n */\nexport function buildConfigArtifacts(options = {}) {\n const root = options.root ?? process.cwd();\n\n const rootConfig = buildRootConfig(options);\n const layoutsConfig = { layouts: LAYOUTS };\n const fallbackLayoutsConfig = { layouts: FALLBACK_LAYOUTS };\n\n const sourceViewerHtml = generateSourceViewerHtml(root);\n const docsHtml = {\n sourceViewer: { pt: sourceViewerHtml, en: sourceViewerHtml, es: sourceViewerHtml },\n };\n\n const versionConfigs = {};\n for (const versionId of DOC_VERSIONS) {\n versionConfigs[versionId] = buildVersionConfig(versionId);\n }\n\n return {\n rootConfig,\n layoutsConfig,\n fallbackLayoutsConfig,\n docs: DOCS,\n docsHtml,\n versionConfigs,\n };\n}\n","cli/builders/root-config-builder.mjs":"/** Build root config for gitpagedocs */\r\nimport { OFFICIAL_LAYOUTS_CONFIG_URL, OFFICIAL_LAYOUTS_TEMPLATES_URL } from \"../data/urls.mjs\";\r\nimport { DOCS } from \"../content/docs.mjs\";\r\nimport { DOC_VERSIONS } from \"../data/version-constants.mjs\";\r\nimport { defaultLangMenu } from \"../data/i18n-langmenu.mjs\";\r\nimport { defaultTranslations } from \"../data/i18n-translations.mjs\";\r\nimport { getDefaultSiteConfig } from \"../data/default-site-config.mjs\";\r\n\r\nexport function buildRootConfig(options = {}) {\r\n const useLocalLayoutConfig = Boolean(options.useLocalLayoutConfig);\r\n const useOfficialLayouts = !useLocalLayoutConfig;\r\n const githubOwner = options.githubOwner;\r\n const githubRepo = options.githubRepo;\r\n const repositorySearchHome = githubOwner && githubRepo ? false : true;\r\n const renderingUrl =\r\n githubOwner && githubRepo\r\n ? `https://${githubOwner}.github.io/${githubRepo}/`\r\n : \"https://vidigal-code.github.io/git-page-docs/\";\r\n const projectLink =\r\n githubOwner && githubRepo\r\n ? `https://github.com/${githubOwner}/${githubRepo}`\r\n : \"https://github.com/Vidigal-code/git-page-docs\";\r\n\r\n const versionEntries = DOC_VERSIONS.map((id) => ({\r\n id,\r\n path: `gitpagedocs/docs/versions/${id}/config.json`,\r\n ProjectLink: projectLink,\r\n PathConfig: `gitpagedocs/docs/versions/${id}/config.json`,\r\n PreviewProject: \"\",\r\n UpdateDate: \"\",\r\n branch: \"\",\r\n release: \"\",\r\n commit: \"\",\r\n }));\r\n\r\n const baseSiteConfig = getDefaultSiteConfig(DOCS, projectLink);\r\n\r\n return {\r\n site: {\r\n ...baseSiteConfig,\r\n layoutsConfigPathOficial: useOfficialLayouts,\r\n layoutsConfigPathTemplatesOficial: useOfficialLayouts ? OFFICIAL_LAYOUTS_TEMPLATES_URL : \"\",\r\n layoutsConfigPathOficialUrl: useOfficialLayouts ? OFFICIAL_LAYOUTS_CONFIG_URL : \"\",\r\n repositorySearchHome,\r\n rendering: renderingUrl,\r\n AiChatEnabled: true,\r\n langmenu: defaultLangMenu,\r\n },\r\n VersionControl: {\r\n versions: versionEntries,\r\n },\r\n translations: defaultTranslations,\r\n };\r\n}\r\n","cli/builders/route-builders.mjs":"/** Build route objects for md, html, and video content types */\n\nexport function buildMdRoute(versionId, routeId, pathByLang, titles, descriptions, options = {}) {\n const {\n titleCss = \"font-size: 1.85rem; font-weight: 700;\",\n titleDarkCss = \"font-size: 1.85rem; font-weight: 700; color: var(--text);\",\n titleLightCss = \"font-size: 1.85rem; font-weight: 700; color: var(--text);\",\n titlePosition = \"center\",\n titleIsVisible = true,\n descriptionCss = \"font-size: 1.2rem; font-weight: 500;\",\n descriptionDarkCss = \"font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);\",\n descriptionLightCss = \"font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);\",\n descriptionPosition = \"center\",\n descriptionIsVisible = true,\n fullscreenEnabled = true,\n marginTop = \"\",\n marginBottom = \"\",\n blockLink = true,\n container,\n browseAll = false,\n RouteguideBrand = true,\n RouteGuideSpeciFicbrand = [],\n RouteguideBrandPosition = \"center\",\n RouteguideBrandContainerTop = false,\n audio,\n authorization,\n } = options;\n const out = {\n id: routeId,\n title: titles ?? { pt: \"Documentação\", en: \"Documentation\", es: \"Documentación\" },\n description: descriptions ?? { pt: \"Descrição da página\", en: \"Page description\", es: \"Descripción de la página\" },\n titleCss,\n titleDarkCss,\n titleLightCss,\n titlePosition,\n titleIsVisible,\n descriptionCss,\n descriptionDarkCss,\n descriptionLightCss,\n descriptionPosition,\n descriptionIsVisible,\n path: pathByLang,\n fullscreenEnabled,\n marginTop,\n marginBottom,\n blockLink,\n container,\n browseAll,\n RouteguideBrand,\n RouteGuideSpeciFicbrand,\n RouteguideBrandPosition,\n RouteguideBrandContainerTop,\n };\n if (container !== undefined) out.container = container;\n if (audio !== undefined) out.audio = audio;\n if (authorization !== undefined) out.authorization = authorization;\n return out;\n}\n\nexport function buildHtmlRoute(versionId, routeId, pathByLang, titles, descriptions, options = {}) {\n const base = buildMdRoute(versionId, routeId, pathByLang, titles, descriptions, options);\n return { ...base };\n}\n\nexport function buildVideoRoute(versionId, routeId, videoType, pathVideo, titles, descriptions, options = {}) {\n const {\n titleCss = \"font-size: 1.85rem; font-weight: 700;\",\n titleDarkCss = \"font-size: 1.85rem; font-weight: 700; color: var(--text);\",\n titleLightCss = \"font-size: 1.85rem; font-weight: 700; color: var(--text);\",\n titlePosition = \"center\",\n titleIsVisible = true,\n descriptionCss = \"font-size: 1.2rem; font-weight: 500;\",\n descriptionDarkCss = \"font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);\",\n descriptionLightCss = \"font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);\",\n descriptionPosition = \"center\",\n descriptionIsVisible = true,\n fullscreenEnabled = true,\n marginTop = \"\",\n marginBottom = \"\",\n blockLink = true,\n container,\n browseAll = false,\n authorization,\n } = options;\n const videoTypeByLang = typeof videoType === \"string\" ? { pt: videoType, en: videoType, es: videoType } : videoType;\n const pathVideoByLang = typeof pathVideo === \"string\" ? { pt: pathVideo, en: pathVideo, es: pathVideo } : pathVideo;\n const obj = {\n id: routeId,\n title: titles ?? { pt: \"Vídeo\", en: \"Video\", es: \"Vídeo\" },\n description: descriptions ?? { pt: \"Descrição do vídeo\", en: \"Video description\", es: \"Descripción del vídeo\" },\n titleCss,\n titleDarkCss,\n titleLightCss,\n titlePosition,\n titleIsVisible,\n descriptionCss,\n descriptionDarkCss,\n descriptionLightCss,\n descriptionPosition,\n descriptionIsVisible,\n fullscreenEnabled,\n marginTop,\n marginBottom,\n blockLink,\n browseAll,\n video: { videoType: videoTypeByLang, pathVideo: pathVideoByLang },\n };\n if (container !== undefined) obj.container = container;\n if (authorization !== undefined) obj.authorization = authorization;\n return obj;\n}\n","cli/builders/source-viewer.mjs":"/** Generate GitHub-style source code viewer HTML from src/, cli/, and root config files */\n\nimport path from \"node:path\";\nimport { readFileSync, readdirSync, existsSync } from \"node:fs\";\n\nfunction buildTreeFromKeys(fileKeys) {\n const root = {};\n for (const key of fileKeys) {\n const parts = key.split(\"/\");\n let curr = root;\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const isLast = i === parts.length - 1;\n if (!curr[part]) curr[part] = isLast ? { path: key } : {};\n if (isLast) curr[part].path = key;\n else curr = curr[part];\n }\n }\n return root;\n}\n\nconst ROOT_FILES = [\"README.md\", \"package.json\", \"package-lock.json\", \"next.config.ts\", \"tsconfig.json\", \".eslintrc.json\"];\n\nfunction collectSourceFiles(projectRoot) {\n const files = {};\n function scan(dir, prefix = \"\") {\n if (!existsSync(dir)) return;\n const entries = readdirSync(dir, { withFileTypes: true })\n .sort((a, b) => a.name.localeCompare(b.name));\n for (const e of entries) {\n const name = e.name;\n if (name === \"node_modules\" || name.startsWith(\".\")) continue;\n const rel = prefix ? `${prefix}/${name}` : name;\n const fullPath = path.join(dir, name);\n if (e.isDirectory()) {\n scan(fullPath, rel);\n } else {\n try {\n const content = readFileSync(fullPath, \"utf-8\");\n files[rel] = content;\n } catch {\n /* skip binary or unreadable */\n }\n }\n }\n }\n for (const f of ROOT_FILES) {\n const p = path.join(projectRoot, f);\n if (existsSync(p)) {\n try {\n files[f] = readFileSync(p, \"utf-8\");\n } catch {}\n }\n }\n const srcDir = path.join(projectRoot, \"src\");\n const cliDir = path.join(projectRoot, \"cli\");\n if (existsSync(srcDir)) scan(srcDir, \"src\");\n if (existsSync(cliDir)) scan(cliDir, \"cli\");\n return files;\n}\n\n/** GitHub-like file viewer: breadcrumb bar, file header, table layout, dark theme */\nexport function generateSourceViewerHtml(root) {\n const files = collectSourceFiles(root);\n const fileKeys = Object.keys(files).sort();\n const tree = buildTreeFromKeys(fileKeys);\n const noFilesMsg = \"No files found.\";\n const dataJson = JSON.stringify({ files, fileKeys, tree });\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\"/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n<title>Source code - GitHub\n\n + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.0.0/en/source-viewer.html b/tools/gitpagedocs/docs/versions/1.0.0/en/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/en/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.0.0/es/authorized-routes.md new file mode 100644 index 0000000..ed32528 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/authorized-routes.md @@ -0,0 +1,55 @@ +# Rutas autorizadas + +Protege rutas por clave de acceso, roles requeridos y proveedores externos. + +## Ubicacion del config de version + +Configura en: + +- `gitpagedocs/docs/versions//config.json` + +## Seccion global auth + +Usa `auth` en la raiz del config de version: + +- `accessKeys`: mapa de ids de clave al secreto esperado +- `rolesStorageKey`: clave de localStorage para bootstrap de roles +- `providers`: lista de proveedores externos (`authjs`, `clerk`, `firebase`, `jwt`) + +## Autorizacion por ruta + +Dentro de cada ruta (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Fases + +### Fase A - Clave de acceso + +Define `authorization.accessKeyId` y la clave correspondiente en `auth.accessKeys`. + +### Fase B - Roles + +Define `authorization.requiredRoles` con uno o mas roles. + +Los roles pueden venir de: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- claims de proveedores externos + +### Fase C - Proveedores externos + +Define `authorization.requireExternalAuth=true` y opcionalmente `allowedProviders`. + +Adaptadores soportados: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- JWT custom (`type: "jwt"`) + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/functionalities.md b/tools/gitpagedocs/docs/versions/1.0.0/es/functionalities.md new file mode 100644 index 0000000..a7a9864 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/functionalities.md @@ -0,0 +1,68 @@ +# Funcionalidades + +Referencia completa de opciones CLI, claves de configuracion y funciones del runtime. + +## Comandos CLI + +| Comando | Descripcion | +|---------|-------------| +| `npx gitpagedocs` | Genera config y docs en `gitpagedocs/` | +| `npx gitpagedocs --layoutconfig` | Tambien genera layouts/templates locales | +| `npx gitpagedocs --home` | Distribucion standalone (`gitpagedocshome/`) | +| `npx gitpagedocs --push --owner X --repo Y` | Configura workflow, commit, push | +| `npx gitpagedocs --interactive` / `-i` | Modo interactivo con prompts | + +## Opciones CLI + +| Opcion | Descripcion | +|--------|-------------| +| `--owner ` | Owner de GitHub | +| `--repo ` | Repositorio GitHub | +| `--path ` | Subruta de docs (ej: `docs`); sin ella, base path = nombre del repo para CSS/JS en project sites | +| `--output ` | Directorio de salida (default: `gitpagedocs`) | +| `--search true|false` | Habilita/deshabilita busqueda de repositorio (`--home`) | +| `--layoutconfig` | Genera `gitpagedocs/layouts/` | +| `--push` | Crea workflow, commit de artefactos, push | +| `--home` | Genera `gitpagedocshome/` (estatico + .env + Dockerfile) | + +## Salida generada + +- `gitpagedocs/config.json` – config raiz +- `gitpagedocs/icon.svg` – icono por defecto +- `gitpagedocs/docs/versions//config.json` – rutas por version +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – docs en markdown +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – visor de codigo (estilo GitHub) +- `gitpagedocs/layouts/` – solo con `--layoutconfig` + +## Tipos de contenido + +| Tipo | Clave config | Descripcion | +|------|--------------|-------------| +| Markdown | `routes-md` | Archivos .md con `path` por idioma | +| HTML | `routes-html` | `path` (ej: source-viewer) o `url` externa | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Visor de codigo fuente + +La CLI genera una pagina **Codigo fuente** por version. Escanea `src/`, `cli/` y archivos raiz (README.md, package.json, next.config.ts, etc.) y construye un visor estilo GitHub en modo oscuro con: + +- Arbol de archivos en barra lateral con expandir/colapsar carpetas +- Filtro de busqueda +- Resaltado de sintaxis (TypeScript, JavaScript, JSON, CSS, Markdown) +- Boton copiar, numeros de linea +- Alternar vista previa/codigo del README.md +- Controles Expandir todo / Colapsar todo + +## Claves de config (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Variables de entorno + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – busqueda de repositorio (local) +- `GITHUB_ACTIONS` – modo build GitHub Pages + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/getting-started.md b/tools/gitpagedocs/docs/versions/1.0.0/es/getting-started.md new file mode 100644 index 0000000..dd47e5b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/getting-started.md @@ -0,0 +1,40 @@ +# Primeros pasos + +Esta guia lleva el proyecto desde cero hasta docs corriendo. + +## Requisitos + +- Node.js 20+ +- npm 10+ (o pnpm) + +## Setup local + +1. Instala dependencias: + - `npm install` +2. Genera/actualiza artefactos de docs: + - `npm run gitpagedocs` +3. Inicia desarrollo: + - `npm run dev` +4. Build + ejecucion local de produccion: + - `npm run build` + - `npm start` + +## Comportamiento de la CLI + +`npx gitpagedocs` (o `npm run gitpagedocs`) genera artefactos en la carpeta oficial `gitpagedocs/`. + +- Genera solo markdown/json +- No genera `index.html` +- No genera `index.js` +- No ejecuta comandos de instalacion + +## Modo de busqueda por repositorio + +En local, se controla por variable: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +En build de GitHub Pages (`GITHUB_ACTIONS=true`), la busqueda de repositorio siempre esta activa. + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/git-introduction.md b/tools/gitpagedocs/docs/versions/1.0.0/es/git-introduction.md new file mode 100644 index 0000000..f9f797e --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/git-introduction.md @@ -0,0 +1,12 @@ +# Introduccion a Git + +Conceptos basicos de Git para principiantes. + +## Comandos esenciales + +- `git init` - iniciar repositorio +- `git add` - preparar cambios +- `git commit` - registrar commit +- `git push` - enviar a remoto + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.0.0/es/github-issues-projects.md new file mode 100644 index 0000000..1d31d27 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/github-issues-projects.md @@ -0,0 +1,11 @@ +# GitHub Issues y Projects + +Aprende a usar GitHub Issues y Projects para gestionar tu trabajo. + +## Conceptos + +- Issues para rastrear tareas y bugs +- Projects para visualizar y organizar el trabajo +- Flujos recomendados para equipos + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/index.md b/tools/gitpagedocs/docs/versions/1.0.0/es/index.md new file mode 100644 index 0000000..04f7fad --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs es un runtime de documentacion multilenguaje para repositorios que incluyen la carpeta `gitpagedocs/`. + +## Que entrega este proyecto + +- Renderizado markdown multilenguaje (`en`, `pt`, `es`) +- Ruteo por version (`/v/:version`) +- Sistema de temas con templates JSON +- Ejecucion local y en GitHub Pages +- Busqueda de repositorio + render remoto opcional + +## Contrato de carpetas + +El runtime espera esta estructura: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Navegacion rapida + +- Abre **Primeros pasos** para setup local. +- Abre **Configuracion** para detalle completo de `config.json`. +- Abre **Publicacion** para comportamiento local/produccion/GitHub Pages. +- Abre **Arquitectura** para mapa de codigo y flujo de datos. +- Abre **Temas y layouts** para creacion de templates. +- Abre **Rutas autorizadas** para configurar clave, roles y autenticacion externa. +- Abre **FAQ** para troubleshooting. + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/project-overview.md b/tools/gitpagedocs/docs/versions/1.0.0/es/project-overview.md new file mode 100644 index 0000000..bd485e2 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/project-overview.md @@ -0,0 +1,27 @@ +# Vision general del proyecto + +Git Page Docs es un monorepo pnpm + turborepo que convierte la carpeta `gitpagedocs/` de un repositorio en un sitio de documentacion multilingue y versionado — con un asistente de IA integrado y un servidor MCP (Model Context Protocol). + +## Paquetes del monorepo + +- **frontend/** — visor Next.js 15 (App Router, React 19), exportado estaticamente para GitHub Pages. +- **cli/** — el paquete npm publicado `gitpagedocs` (`npm install -g gitpagedocs`): genera la estructura de docs, documenta con IA, configura Pages y ejecuta el servidor MCP. +- **tools/** — `@gitpagedocs/tools`, el nucleo de logica compartido: sistema de IA con 14 proveedores, boveda de credenciales cifrada, cargador de config, caches y logger. +- **mcp/** — `@gitpagedocs/mcp`, servidor Model Context Protocol (20 herramientas + 7 recursos). +- **gitpagedocs/** — el contrato del usuario: `config.json`, docs versionados y layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; exportacion estatica para GitHub Pages +- gray-matter + marked para Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Destacados + +- Multilingue (`en`, `pt`, `es`) y rutas por version (`/v/:version`) +- Sistema de IA con 14 proveedores (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI y mas) con streaming +- Claves de IA **cifradas en reposo** (AES-256-GCM) detras de una contrasena local — nunca en texto plano +- **Panel de chat de IA** en los docs + una **consola `/ai`** dedicada +- Sistema de 36 temas; ejecucion local y en GitHub Pages + +> Version (ES): 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/source-viewer b/tools/gitpagedocs/docs/versions/1.0.0/es/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.0.0/es/source-viewer.html b/tools/gitpagedocs/docs/versions/1.0.0/es/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/es/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/authorized-routes.md new file mode 100644 index 0000000..87b46be --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/authorized-routes.md @@ -0,0 +1,55 @@ +# Rotas autorizadas + +Proteja rotas por chave de acesso, papeis obrigatorios e provedores externos. + +## Local do config de versao + +Configure em: + +- `gitpagedocs/docs/versions//config.json` + +## Secao global auth + +Use `auth` no topo do config de versao: + +- `accessKeys`: mapa de ids de chave para segredo esperado +- `rolesStorageKey`: chave de localStorage para bootstrap de papeis +- `providers`: lista de provedores externos (`authjs`, `clerk`, `firebase`, `jwt`) + +## Autorizacao por rota + +Dentro de cada rota (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Fases + +### Fase A - Chave de acesso + +Defina `authorization.accessKeyId` e a chave correspondente em `auth.accessKeys`. + +### Fase B - Papeis + +Defina `authorization.requiredRoles` com um ou mais papeis. + +Os papeis podem vir de: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- claims de provedores externos + +### Fase C - Provedores externos + +Defina `authorization.requireExternalAuth=true` e opcionalmente `allowedProviders`. + +Adaptadores suportados: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- JWT custom (`type: "jwt"`) + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/functionalities.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/functionalities.md new file mode 100644 index 0000000..446128b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/functionalities.md @@ -0,0 +1,68 @@ +# Funcionalidades + +Referencia completa de opcoes da CLI, chaves de configuracao e recursos do runtime. + +## Comandos da CLI + +| Comando | Descricao | +|---------|------------| +| `npx gitpagedocs` | Gera config e docs em `gitpagedocs/` | +| `npx gitpagedocs --layoutconfig` | Tambem gera layouts/templates locais | +| `npx gitpagedocs --home` | Distribuicao standalone (`gitpagedocshome/`) | +| `npx gitpagedocs --push --owner X --repo Y` | Configura workflow, commit, push | +| `npx gitpagedocs --interactive` / `-i` | Modo interativo com prompts | + +## Opcoes da CLI + +| Opcao | Descricao | +|-------|-----------| +| `--owner ` | Owner do GitHub | +| `--repo ` | Repositorio GitHub | +| `--path ` | Subcaminho dos docs (ex: `docs`); sem ele, base path = nome do repo para CSS/JS em project sites | +| `--output ` | Diretorio de saida (padrao: `gitpagedocs`) | +| `--search true|false` | Habilita/desabilita busca de repositorio (`--home`) | +| `--layoutconfig` | Gera `gitpagedocs/layouts/` | +| `--push` | Cria workflow, commit de artefatos, push | +| `--home` | Gera `gitpagedocshome/` (estatico + .env + Dockerfile) | + +## Saida gerada + +- `gitpagedocs/config.json` – config raiz +- `gitpagedocs/icon.svg` – icone padrao +- `gitpagedocs/docs/versions//config.json` – rotas por versao +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – docs em markdown +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – visualizador de codigo (estilo GitHub) +- `gitpagedocs/layouts/` – apenas com `--layoutconfig` + +## Tipos de conteudo + +| Tipo | Chave config | Descricao | +|------|--------------|-----------| +| Markdown | `routes-md` | Arquivos .md com `path` por idioma | +| HTML | `routes-html` | `path` (ex: source-viewer) ou `url` externa | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Visualizador de codigo fonte + +A CLI gera uma pagina **Codigo fonte** por versao. Escaneia `src/`, `cli/` e arquivos raiz (README.md, package.json, next.config.ts, etc.) e constroi um visualizador estilo GitHub em modo escuro com: + +- Arvore de arquivos na lateral com expandir/recolher pastas +- Filtro de busca +- Destaque de sintaxe (TypeScript, JavaScript, JSON, CSS, Markdown) +- Botao copiar, numeros de linha +- Alternar preview/codigo do README.md +- Controles Expandir tudo / Recolher tudo + +## Chaves de config (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Variaveis de ambiente + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – busca de repositorio (local) +- `GITHUB_ACTIONS` – modo build GitHub Pages + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/getting-started.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/getting-started.md new file mode 100644 index 0000000..e844951 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/getting-started.md @@ -0,0 +1,40 @@ +# Primeiros passos + +Este guia leva o projeto do zero ate docs rodando. + +## Pre-requisitos + +- Node.js 20+ +- npm 10+ (ou pnpm) + +## Setup local + +1. Instale dependencias: + - `npm install` +2. Gere/atualize os artefatos de docs: + - `npm run gitpagedocs` +3. Inicie o desenvolvimento: + - `npm run dev` +4. Build e execucao local de producao: + - `npm run build` + - `npm start` + +## Comportamento da CLI + +`npx gitpagedocs` (ou `npm run gitpagedocs`) gera os artefatos na pasta oficial `gitpagedocs/`. + +- Gera somente markdown/json +- Nao gera `index.html` +- Nao gera `index.js` +- Nao executa comandos de instalacao + +## Modo de busca por repositorio + +No ambiente local, o controle e por variavel: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +Em build de GitHub Pages (`GITHUB_ACTIONS=true`), a busca de repositorio fica sempre ativa. + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/git-introduction.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/git-introduction.md new file mode 100644 index 0000000..378088a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/git-introduction.md @@ -0,0 +1,12 @@ +# Introducao ao Git + +Conceitos basicos de Git para iniciantes. + +## Comandos essenciais + +- `git init` - iniciar repositorio +- `git add` - preparar alteracoes +- `git commit` - registrar commit +- `git push` - enviar para remoto + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/github-issues-projects.md new file mode 100644 index 0000000..7ceca08 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/github-issues-projects.md @@ -0,0 +1,11 @@ +# GitHub Issues e Projects + +Aprenda a usar GitHub Issues e Projects para gerenciar seu trabalho. + +## Conceitos + +- Issues para rastrear tarefas e bugs +- Projects para visualizar e organizar o trabalho +- Workflows recomendados para equipes + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/index.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/index.md new file mode 100644 index 0000000..004ce74 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs e um runtime de documentacao multi-idioma para repositorios que possuem a pasta `gitpagedocs/`. + +## O que este projeto entrega + +- Renderizacao markdown em varios idiomas (`en`, `pt`, `es`) +- Roteamento por versao (`/v/:versao`) +- Sistema de temas por templates JSON +- Execucao local e em GitHub Pages +- Busca de repositorio + renderizacao remota opcional + +## Contrato de pastas + +O runtime espera esta estrutura: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Navegacao rapida + +- Abra **Primeiros passos** para setup local. +- Abra **Configuracao** para detalhes completos do `config.json`. +- Abra **Publicacao** para comportamento local/producao/GitHub Pages. +- Abra **Arquitetura** para mapa de codigo e fluxo de dados. +- Abra **Temas e layouts** para autoria de templates. +- Abra **Rotas autorizadas** para configurar chave, papeis e autenticacao externa. +- Abra **FAQ** para troubleshooting. + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/project-overview.md b/tools/gitpagedocs/docs/versions/1.0.0/pt/project-overview.md new file mode 100644 index 0000000..4aa7e06 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/project-overview.md @@ -0,0 +1,27 @@ +# Visao geral do projeto + +Git Page Docs e um monorepo pnpm + turborepo que transforma a pasta `gitpagedocs/` de um repositorio em um site de documentacao multilinguagem e versionado — com assistente de IA integrado e um servidor MCP (Model Context Protocol). + +## Pacotes do monorepo + +- **frontend/** — visualizador Next.js 15 (App Router, React 19), exportado estaticamente para o GitHub Pages. +- **cli/** — o pacote npm publicado `gitpagedocs` (`npm install -g gitpagedocs`): gera a estrutura de docs, documenta com IA, configura o Pages e roda o servidor MCP. +- **tools/** — `@gitpagedocs/tools`, o nucleo de logica compartilhado: sistema de IA com 14 provedores, cofre de credenciais criptografado, loader de config, caches e logger. +- **mcp/** — `@gitpagedocs/mcp`, servidor Model Context Protocol (20 ferramentas + 7 recursos). +- **gitpagedocs/** — o contrato do usuario: `config.json`, docs versionados e layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; exportacao estatica para GitHub Pages +- gray-matter + marked para Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Destaques + +- Multilinguagem (`en`, `pt`, `es`) e rotas por versao (`/v/:version`) +- Sistema de IA com 14 provedores (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI e mais) com streaming +- Chaves de IA **criptografadas em repouso** (AES-256-GCM) atras de uma senha local — nunca em texto puro +- **Drawer de chat de IA** nos docs + um **console `/ai`** dedicado +- Sistema de 36 temas; execucao local e no GitHub Pages + +> Versao: 1.0.0 diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/source-viewer b/tools/gitpagedocs/docs/versions/1.0.0/pt/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.0.0/pt/source-viewer.html b/tools/gitpagedocs/docs/versions/1.0.0/pt/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.0.0/pt/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.0/config.json b/tools/gitpagedocs/docs/versions/1.1.0/config.json new file mode 100644 index 0000000..24a2eac --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/config.json @@ -0,0 +1,716 @@ +{ + "auth": { + "accessKeys": { + "docs-key": "open-gitpagedocs-docs", + "source-viewer-key": "open-source-viewer" + }, + "rolesStorageKey": "git-page-docs:route-auth:roles", + "providers": [ + { + "type": "authjs", + "enabled": true, + "sessionEndpoint": "/api/auth/session", + "rolesClaimPath": "user.roles" + }, + { + "type": "clerk", + "enabled": true, + "rolesClaimPath": "claims.publicMetadata.roles" + }, + { + "type": "firebase", + "enabled": true, + "tokenStorageKey": "git-page-docs:firebase-token", + "rolesClaimPath": "roles" + }, + { + "type": "jwt", + "enabled": true, + "tokenStorageKey": "git-page-docs:jwt-token", + "rolesClaimPath": "roles" + } + ] + }, + "routes-md": [ + { + "id": 1, + "title": { + "pt": "Primeiros passos", + "en": "Getting Started", + "es": "Primeros pasos" + }, + "description": { + "pt": "Configure o repositório do zero", + "en": "Configure repository from zero", + "es": "Configura el repositorio desde cero" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/getting-started.md", + "en": "gitpagedocs/docs/versions/1.1.0/en/getting-started.md", + "es": "gitpagedocs/docs/versions/1.1.0/es/getting-started.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false + }, + { + "id": 2, + "title": { + "pt": "Visão geral do projeto", + "en": "Project overview", + "es": "Visión general del proyecto" + }, + "description": { + "pt": "Stack, objetivo e estrutura do Git Page Docs", + "en": "Stack, goals and structure of Git Page Docs", + "es": "Stack, objetivos y estructura de Git Page Docs" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/project-overview.md", + "en": "gitpagedocs/docs/versions/1.1.0/en/project-overview.md", + "es": "gitpagedocs/docs/versions/1.1.0/es/project-overview.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "audio": { + "enabled": true, + "autoPlayOnLoad": true, + "loopEnabled": false, + "tracks": [ + { + "url": "https://www.youtube.com/watch?v=0w80F8FffQ4", + "type": "youtube", + "title": { + "pt": "Música página 2", + "en": "Page 2 music", + "es": "Música página 2" + } + } + ] + } + }, + { + "id": 3, + "title": { + "pt": "Funcionalidades", + "en": "Functionalities", + "es": "Funcionalidades" + }, + "description": { + "pt": "CLI, opções, configuração e recursos", + "en": "CLI, options, configuration and features", + "es": "CLI, opciones, configuración y funciones" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/functionalities.md", + "en": "gitpagedocs/docs/versions/1.1.0/en/functionalities.md", + "es": "gitpagedocs/docs/versions/1.1.0/es/functionalities.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "authorization": { + "requiredRoles": [ + "maintainer" + ] + } + }, + { + "id": 4, + "title": { + "pt": "GitHub Issues e Projects", + "en": "GitHub issues and projects", + "es": "GitHub issues y projects" + }, + "description": { + "pt": "Como usar Issues e Projects no GitHub", + "en": "How to use GitHub Issues and Projects", + "es": "Cómo usar GitHub Issues y Projects" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/github-issues-projects.md", + "en": "gitpagedocs/docs/versions/1.1.0/en/github-issues-projects.md", + "es": "gitpagedocs/docs/versions/1.1.0/es/github-issues-projects.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false + }, + { + "id": 5, + "title": { + "pt": "Introdução ao Git", + "en": "Introduction to Git", + "es": "Introducción a Git" + }, + "description": { + "pt": "Conceitos básicos de Git para iniciantes", + "en": "Basic Git concepts for beginners", + "es": "Conceptos básicos de Git para principiantes" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/git-introduction.md", + "en": "gitpagedocs/docs/versions/1.1.0/en/git-introduction.md", + "es": "gitpagedocs/docs/versions/1.1.0/es/git-introduction.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false + }, + { + "id": 6, + "title": { + "pt": "Rotas autorizadas", + "en": "Authorized routes", + "es": "Rutas autorizadas" + }, + "description": { + "pt": "Controle acesso por chave, roles e provedores externos", + "en": "Control access by key, roles and external providers", + "es": "Controla acceso por clave, roles y proveedores externos" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/authorized-routes.md", + "en": "gitpagedocs/docs/versions/1.1.0/en/authorized-routes.md", + "es": "gitpagedocs/docs/versions/1.1.0/es/authorized-routes.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "authorization": { + "accessKeyId": "docs-key", + "requiredRoles": [ + "maintainer" + ], + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + } + ], + "routes-html": [ + { + "id": 1, + "title": { + "pt": "Código fonte", + "en": "Source code", + "es": "Código fuente" + }, + "description": { + "pt": "Visualizar código-fonte do projeto", + "en": "View project source code", + "es": "Ver código fuente del proyecto" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.0/pt/source-viewer", + "en": "gitpagedocs/docs/versions/1.1.0/en/source-viewer", + "es": "gitpagedocs/docs/versions/1.1.0/es/source-viewer" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "container": "full", + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "authorization": { + "accessKeyId": "source-viewer-key" + } + } + ], + "routes-video": [ + { + "id": 1, + "title": { + "pt": "Interactive vs non-interactive modes | Copilot CLI for beginners", + "en": "Interactive vs non-interactive modes | Copilot CLI for beginners", + "es": "Interactive vs non-interactive modes | Copilot CLI for beginners" + }, + "description": { + "pt": "Quer saber a forma mais rápida de usar o GitHub Copilot no terminal? Neste tutorial, exploramos os dois modos principais do Copilot CLI.", + "en": "Want to know the fastest way to prompt GitHub Copilot from your terminal? In this beginner tutorial, we explore the two main modes of the Copilot CLI. Discover how to use the interactive mode to have GitHub Copilot run your project locally or use the non-interactive mode with the -p flag for quick summaries without leaving your shell context.", + "es": "¿Quieres conocer la forma más rápida de usar GitHub Copilot desde tu terminal? En este tutorial exploramos los dos modos principales del Copilot CLI." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "bdIJkGr2NV0", + "en": "bdIJkGr2NV0", + "es": "bdIJkGr2NV0" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + }, + { + "id": 2, + "title": { + "pt": "How to use GitHub issues and projects | GitHub for Beginners", + "en": "How to use GitHub issues and projects | GitHub for Beginners", + "es": "How to use GitHub issues and projects | GitHub for Beginners" + }, + "description": { + "pt": "Aprenda a usar GitHub Issues e Projects para gerenciar seu trabalho.", + "en": "Learn how to use GitHub Issues and Projects to manage your work effectively.", + "es": "Aprende a usar GitHub Issues y Projects para gestionar tu trabajo." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "c67GaAkf1BE", + "en": "c67GaAkf1BE", + "es": "c67GaAkf1BE" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + }, + { + "id": 3, + "title": { + "pt": "How I built an AI Python tutor with the GitHub Copilot SDK", + "en": "How I built an AI Python tutor with the GitHub Copilot SDK", + "es": "How I built an AI Python tutor with the GitHub Copilot SDK" + }, + "description": { + "pt": "Construindo um tutor de Python com IA usando o GitHub Copilot SDK.", + "en": "Building an AI Python tutor using the GitHub Copilot SDK.", + "es": "Construyendo un tutor de Python con IA usando el GitHub Copilot SDK." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "N3my6W_Rdwg", + "en": "N3my6W_Rdwg", + "es": "N3my6W_Rdwg" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + }, + { + "id": 4, + "title": { + "pt": "A brief introduction to Git for beginners | GitHub", + "en": "A brief introduction to Git for beginners | GitHub", + "es": "A brief introduction to Git for beginners | GitHub" + }, + "description": { + "pt": "Introdução ao Git para iniciantes.", + "en": "A brief introduction to Git for beginners.", + "es": "Una breve introducción a Git para principiantes." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "r8jQ9hVA2qs", + "en": "r8jQ9hVA2qs", + "es": "r8jQ9hVA2qs" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + } + ], + "menus-header-md": [ + { + "id": 1, + "pt": { + "title": "Primeiros passos", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/getting-started.md" + }, + "en": { + "title": "Getting Started", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/getting-started.md" + }, + "es": { + "title": "Primeros pasos", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/getting-started.md" + } + }, + { + "id": 2, + "pt": { + "title": "Visão geral do projeto", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/project-overview.md" + }, + "en": { + "title": "Project overview", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/project-overview.md" + }, + "es": { + "title": "Visión general del proyecto", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/project-overview.md" + } + }, + { + "id": 3, + "pt": { + "title": "Funcionalidades", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/functionalities.md" + }, + "en": { + "title": "Functionalities", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/functionalities.md" + }, + "es": { + "title": "Funcionalidades", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/functionalities.md" + } + }, + { + "id": 4, + "pt": { + "title": "GitHub Issues e Projects", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/github-issues-projects.md" + }, + "en": { + "title": "GitHub issues and projects", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/github-issues-projects.md" + }, + "es": { + "title": "GitHub issues y projects", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/github-issues-projects.md" + } + }, + { + "id": 5, + "pt": { + "title": "Introdução ao Git", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/git-introduction.md" + }, + "en": { + "title": "Introduction to Git", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/git-introduction.md" + }, + "es": { + "title": "Introducción a Git", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/git-introduction.md" + } + }, + { + "id": 6, + "pt": { + "title": "Rotas autorizadas", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/authorized-routes.md" + }, + "en": { + "title": "Authorized routes", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/authorized-routes.md" + }, + "es": { + "title": "Rutas autorizadas", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/authorized-routes.md" + } + } + ], + "menus-header-html": [ + { + "id": 1, + "pt": { + "title": "Código fonte", + "path-click": "gitpagedocs/docs/versions/1.1.0/pt/source-viewer" + }, + "en": { + "title": "Source code", + "path-click": "gitpagedocs/docs/versions/1.1.0/en/source-viewer" + }, + "es": { + "title": "Código fuente", + "path-click": "gitpagedocs/docs/versions/1.1.0/es/source-viewer" + } + } + ], + "menus-header-video": [ + { + "id": 1, + "pt": { + "title": "Interactive vs non-interactive modes | C...", + "path-click": "page:1" + }, + "en": { + "title": "Interactive vs non-interactive modes | C...", + "path-click": "page:1" + }, + "es": { + "title": "Interactive vs non-interactive modes | C...", + "path-click": "page:1" + } + }, + { + "id": 2, + "pt": { + "title": "How to use GitHub issues and projects | ...", + "path-click": "page:2" + }, + "en": { + "title": "How to use GitHub issues and projects | ...", + "path-click": "page:2" + }, + "es": { + "title": "How to use GitHub issues and projects | ...", + "path-click": "page:2" + } + }, + { + "id": 3, + "pt": { + "title": "How I built an AI Python tutor with the ...", + "path-click": "page:3" + }, + "en": { + "title": "How I built an AI Python tutor with the ...", + "path-click": "page:3" + }, + "es": { + "title": "How I built an AI Python tutor with the ...", + "path-click": "page:3" + } + }, + { + "id": 4, + "pt": { + "title": "A brief introduction to Git for beginner...", + "path-click": "page:4" + }, + "en": { + "title": "A brief introduction to Git for beginner...", + "path-click": "page:4" + }, + "es": { + "title": "A brief introduction to Git for beginner...", + "path-click": "page:4" + } + } + ], + "hierarchyPage": { + "md": 0, + "html": 1, + "video": 2, + "audio": 3 + }, + "hierarchyMenu": { + "md": 0, + "html": 1, + "video": 2, + "audio": 3 + } +} diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.1.0/en/authorized-routes.md new file mode 100644 index 0000000..e961fda --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/authorized-routes.md @@ -0,0 +1,87 @@ +# Authorized Routes + +Protect routes by access key, required roles, and external authentication providers. + +## Version config location + +Configure this at: + +- `gitpagedocs/docs/versions//config.json` + +## Global auth section + +Use top-level `auth` in version config: + +- `accessKeys`: map of key ids to expected secrets +- `rolesStorageKey`: localStorage key for role bootstrap +- `providers`: external providers list (`authjs`, `clerk`, `firebase`, `jwt`) + +## Route-level authorization + +Inside each route (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Phases + +### Phase A - Access key + +Set `authorization.accessKeyId` and define that key in `auth.accessKeys`. + +### Phase B - Roles + +Set `authorization.requiredRoles` with one or more roles. + +Roles can come from: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- external provider claims + +### Phase C - External providers + +Set `authorization.requireExternalAuth=true` and optionally `allowedProviders`. + +Supported adapters: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- Custom JWT (`type: "jwt"`) + +## Example + +```json +{ + "auth": { + "accessKeys": { + "docs-key": "open-gitpagedocs-docs" + }, + "providers": [ + { "type": "authjs", "enabled": true, "sessionEndpoint": "/api/auth/session" }, + { "type": "jwt", "enabled": true, "tokenStorageKey": "git-page-docs:jwt-token" } + ] + }, + "routes-md": [ + { + "id": 6, + "path": { + "en": "gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md", + "pt": "gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md" + }, + "authorization": { + "accessKeyId": "docs-key", + "requiredRoles": ["maintainer"], + "requireExternalAuth": true, + "allowedProviders": ["authjs", "jwt"] + } + } + ] +} +``` + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/functionalities.md b/tools/gitpagedocs/docs/versions/1.1.0/en/functionalities.md new file mode 100644 index 0000000..7861af0 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/functionalities.md @@ -0,0 +1,90 @@ +# Functionalities + +Complete reference of CLI options, configuration keys, and runtime features. + +## CLI commands + +| Command | Description | +|---------|--------------| +| `gitpagedocs` | Generate config and docs in `gitpagedocs/` | +| `gitpagedocs --layoutconfig` | Also generate local layouts/templates | +| `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`) | +| `gitpagedocs --push --owner X --repo Y` | Setup workflow, commit, push | +| `gitpagedocs --interactive` / `-i` | Interactive mode with prompts | +| `gitpagedocs ai` | Interactive AI documentation generator | +| `gitpagedocs provider [id]` / `models [provider]` | List AI providers / catalog models | +| `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI | +| `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push | +| `gitpagedocs docs` | Refresh README/CONTRIBUTING/SECURITY managed regions | +| `gitpagedocs doctor` / `version` / `update` | Diagnostics / version / update hint | +| `gitpagedocs mcp start` | Start the MCP server over stdio | + +Install globally with `npm install -g gitpagedocs`, or run one-off with `npx gitpagedocs`. + +## CLI options + +| Option | Description | +|--------|-------------| +| `--owner ` | GitHub owner | +| `--repo ` | GitHub repository | +| `--path ` | Docs subpath (e.g. `docs`); without it, base path = repo name for correct CSS/JS on project sites | +| `--output ` | Output directory (default: `gitpagedocs`) | +| `--search true|false` | Enable/disable repository search (`--home`) | +| `--layoutconfig` | Generate `gitpagedocs/layouts/` | +| `--push` | Create workflow, commit artifacts, push | +| `--home` | Generate `gitpagedocshome/` (static + .env + Dockerfile) | + +## Generated output + +- `gitpagedocs/config.json` – root config +- `gitpagedocs/icon.svg` – default icon +- `gitpagedocs/docs/versions//config.json` – per-version routes +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – markdown docs +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – GitHub-style source code viewer +- `gitpagedocs/layouts/` – only with `--layoutconfig` + +## Content types + +| Type | Config key | Description | +|------|------------|-------------| +| Markdown | `routes-md` | .md files with `path` per language | +| HTML | `routes-html` | `path` (e.g. source-viewer) or `url` for external | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Source code viewer + +The CLI generates a **Source code** page per version. It scans `src/`, `cli/`, and root files (README.md, package.json, next.config.ts, etc.) and builds a GitHub-style dark viewer with: + +- File tree sidebar with folder collapse/expand +- Search filter for files +- Syntax highlighting (TypeScript, JavaScript, JSON, CSS, Markdown) +- Copy button, line numbers +- README.md preview/code toggle +- Expand all / Collapse all controls + +## Config keys (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Environment variables + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – repository search (local) +- `GITHUB_ACTIONS` – GitHub Pages build mode + +## AI assistant + +The docs ship an AI assistant in two surfaces: an in-docs **chat drawer** (the ✨ button in the sidebar, enabled via `site.AiChatEnabled`) and a dedicated **`/ai` console** page. + +- **14 providers** via one shared core: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity. +- **Model selection** — pick from each provider's catalog (`gitpagedocs models `) or type a custom id. +- **Encrypted at rest** — your API key is sealed with AES-256-GCM behind a **local password** (one unlock per session) and is never stored in plaintext or logged. A legacy plaintext key is migrated and wiped on first unlock. +- **AI documentation generation** — `gitpagedocs ai` scans chosen paths and writes multilingual markdown (pt/en/es); reusable via `.gitpagedocsconfig`. + +## MCP server + +`gitpagedocs mcp start` runs a Model Context Protocol server (stdio) exposing **20 tools** (filesystem, AI, doc generation/analysis) and **7 resources** (`project://structure|docs|config|repository|readme|ai/providers|ai/models`) for editors and AI agents. + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/getting-started.md b/tools/gitpagedocs/docs/versions/1.1.0/en/getting-started.md new file mode 100644 index 0000000..28acb6e --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/getting-started.md @@ -0,0 +1,45 @@ +# Getting Started + +This guide configures your repository from zero to running docs. + +## Prerequisites + +- Node.js 20+ +- npm 10+ + +## Install and generate + +1. Install package: + - `npm install gitpagedocs` +2. Generate docs config and versions: + - `npx gitpagedocs` +3. Optional: generate local layouts/templates: + - `npx gitpagedocs --layoutconfig` + +## Local run + +1. Development: + - `npm run dev` +2. Production locally: + - `npm run build` + - `npm start` + +## CLI behavior + +`npx gitpagedocs` generates only artifacts in `gitpagedocs/`: + +- JSON + markdown docs assets +- No `index.html` +- No `index.js` +- No install command execution + +## Repository search mode + +Local repository search is controlled by: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +On GitHub Pages builds (`GITHUB_ACTIONS=true`), repository-search home is enabled. + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/git-introduction.md b/tools/gitpagedocs/docs/versions/1.1.0/en/git-introduction.md new file mode 100644 index 0000000..ddf698a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/git-introduction.md @@ -0,0 +1,19 @@ +# Introduction to Git + +Basic Git concepts for beginners. + +## Key concepts + +- **Repository**: A project folder tracked by Git +- **Commit**: A snapshot of changes +- **Branch**: Alternative line of development +- **Remote**: Shared repository (e.g. on GitHub) + +## Common commands + +- `git init` - Initialize a repo +- `git add` - Stage changes +- `git commit` - Create a snapshot +- `git push` - Send to remote + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.1.0/en/github-issues-projects.md new file mode 100644 index 0000000..51e8ecf --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/github-issues-projects.md @@ -0,0 +1,17 @@ +# GitHub Issues and Projects + +Learn how to use GitHub Issues and Projects to manage your work. + +## Issues + +- Track bugs, features, and tasks +- Assignees, labels, milestones +- Discussions and linked PRs + +## Projects + +- Kanban boards +- Tables and roadmaps +- Custom fields and automation + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/index.md b/tools/gitpagedocs/docs/versions/1.1.0/en/index.md new file mode 100644 index 0000000..b3ba66b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs is a multilingual documentation runtime for repositories that ship a `gitpagedocs/` folder. + +## What this project delivers + +- Multilingual markdown rendering (`en`, `pt`, `es`) +- Version-aware docs routing (`/v/:version`) +- Theme system with JSON templates +- Local and GitHub Pages execution modes +- Optional repository search + remote rendering + +## Folder contract + +The runtime expects this structure: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Quick navigation + +- Open **Getting Started** for local setup. +- Open **Configuration** for full `config.json` explanation. +- Open **Deployment** for local, server, and GitHub Pages behavior. +- Open **Architecture** for code map and data flow. +- Open **Themes and layouts** for template authoring details. +- Open **Authorized routes** for key, roles, and external auth setup. +- Open **FAQ** for troubleshooting. + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/project-overview.md b/tools/gitpagedocs/docs/versions/1.1.0/en/project-overview.md new file mode 100644 index 0000000..fcbdbf2 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/project-overview.md @@ -0,0 +1,27 @@ +# Project Overview + +Git Page Docs is a **pnpm + turborepo monorepo** that turns a repository's `gitpagedocs/` folder into a multilingual, versioned documentation site — with an integrated AI assistant and a Model Context Protocol (MCP) server. + +## Monorepo packages + +- **frontend/** — Next.js 15 (App Router, React 19) documentation viewer, static-exported for GitHub Pages. +- **cli/** — the published `gitpagedocs` npm package (`npm install -g gitpagedocs`): scaffolds the docs contract, generates docs with AI, configures Pages, and runs the MCP server. +- **tools/** — `@gitpagedocs/tools`, the shared business-logic core: the 14-provider AI system, the encrypted credential vault, the config loader, caches, and the logger. +- **mcp/** — `@gitpagedocs/mcp`, a Model Context Protocol server (20 tools + 7 resources). +- **gitpagedocs/** — the user contract: `config.json`, versioned docs, and layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; static export for GitHub Pages +- gray-matter + marked for Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Highlights + +- Multilingual (`en`, `pt`, `es`) and version-aware routing (`/v/:version`) +- 14-provider AI system (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI, and more) with streaming +- AI API keys stored **encrypted at rest** (AES-256-GCM) behind a local password gate — never plaintext +- In-docs **AI chat drawer** plus a dedicated **`/ai` console** +- 36-theme layout system; local and GitHub Pages execution modes + +> Version: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/source-viewer b/tools/gitpagedocs/docs/versions/1.1.0/en/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.0/en/source-viewer.html b/tools/gitpagedocs/docs/versions/1.1.0/en/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/en/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.1.0/es/authorized-routes.md new file mode 100644 index 0000000..d6d02c2 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/authorized-routes.md @@ -0,0 +1,55 @@ +# Rutas autorizadas + +Protege rutas por clave de acceso, roles requeridos y proveedores externos. + +## Ubicacion del config de version + +Configura en: + +- `gitpagedocs/docs/versions//config.json` + +## Seccion global auth + +Usa `auth` en la raiz del config de version: + +- `accessKeys`: mapa de ids de clave al secreto esperado +- `rolesStorageKey`: clave de localStorage para bootstrap de roles +- `providers`: lista de proveedores externos (`authjs`, `clerk`, `firebase`, `jwt`) + +## Autorizacion por ruta + +Dentro de cada ruta (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Fases + +### Fase A - Clave de acceso + +Define `authorization.accessKeyId` y la clave correspondiente en `auth.accessKeys`. + +### Fase B - Roles + +Define `authorization.requiredRoles` con uno o mas roles. + +Los roles pueden venir de: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- claims de proveedores externos + +### Fase C - Proveedores externos + +Define `authorization.requireExternalAuth=true` y opcionalmente `allowedProviders`. + +Adaptadores soportados: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- JWT custom (`type: "jwt"`) + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/functionalities.md b/tools/gitpagedocs/docs/versions/1.1.0/es/functionalities.md new file mode 100644 index 0000000..4e22d5b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/functionalities.md @@ -0,0 +1,68 @@ +# Funcionalidades + +Referencia completa de opciones CLI, claves de configuracion y funciones del runtime. + +## Comandos CLI + +| Comando | Descripcion | +|---------|-------------| +| `npx gitpagedocs` | Genera config y docs en `gitpagedocs/` | +| `npx gitpagedocs --layoutconfig` | Tambien genera layouts/templates locales | +| `npx gitpagedocs --home` | Distribucion standalone (`gitpagedocshome/`) | +| `npx gitpagedocs --push --owner X --repo Y` | Configura workflow, commit, push | +| `npx gitpagedocs --interactive` / `-i` | Modo interactivo con prompts | + +## Opciones CLI + +| Opcion | Descripcion | +|--------|-------------| +| `--owner ` | Owner de GitHub | +| `--repo ` | Repositorio GitHub | +| `--path ` | Subruta de docs (ej: `docs`); sin ella, base path = nombre del repo para CSS/JS en project sites | +| `--output ` | Directorio de salida (default: `gitpagedocs`) | +| `--search true|false` | Habilita/deshabilita busqueda de repositorio (`--home`) | +| `--layoutconfig` | Genera `gitpagedocs/layouts/` | +| `--push` | Crea workflow, commit de artefactos, push | +| `--home` | Genera `gitpagedocshome/` (estatico + .env + Dockerfile) | + +## Salida generada + +- `gitpagedocs/config.json` – config raiz +- `gitpagedocs/icon.svg` – icono por defecto +- `gitpagedocs/docs/versions//config.json` – rutas por version +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – docs en markdown +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – visor de codigo (estilo GitHub) +- `gitpagedocs/layouts/` – solo con `--layoutconfig` + +## Tipos de contenido + +| Tipo | Clave config | Descripcion | +|------|--------------|-------------| +| Markdown | `routes-md` | Archivos .md con `path` por idioma | +| HTML | `routes-html` | `path` (ej: source-viewer) o `url` externa | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Visor de codigo fuente + +La CLI genera una pagina **Codigo fuente** por version. Escanea `src/`, `cli/` y archivos raiz (README.md, package.json, next.config.ts, etc.) y construye un visor estilo GitHub en modo oscuro con: + +- Arbol de archivos en barra lateral con expandir/colapsar carpetas +- Filtro de busqueda +- Resaltado de sintaxis (TypeScript, JavaScript, JSON, CSS, Markdown) +- Boton copiar, numeros de linea +- Alternar vista previa/codigo del README.md +- Controles Expandir todo / Colapsar todo + +## Claves de config (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Variables de entorno + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – busqueda de repositorio (local) +- `GITHUB_ACTIONS` – modo build GitHub Pages + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/getting-started.md b/tools/gitpagedocs/docs/versions/1.1.0/es/getting-started.md new file mode 100644 index 0000000..88adaf8 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/getting-started.md @@ -0,0 +1,40 @@ +# Primeros pasos + +Esta guia lleva el proyecto desde cero hasta docs corriendo. + +## Requisitos + +- Node.js 20+ +- npm 10+ (o pnpm) + +## Setup local + +1. Instala dependencias: + - `npm install` +2. Genera/actualiza artefactos de docs: + - `npm run gitpagedocs` +3. Inicia desarrollo: + - `npm run dev` +4. Build + ejecucion local de produccion: + - `npm run build` + - `npm start` + +## Comportamiento de la CLI + +`npx gitpagedocs` (o `npm run gitpagedocs`) genera artefactos en la carpeta oficial `gitpagedocs/`. + +- Genera solo markdown/json +- No genera `index.html` +- No genera `index.js` +- No ejecuta comandos de instalacion + +## Modo de busqueda por repositorio + +En local, se controla por variable: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +En build de GitHub Pages (`GITHUB_ACTIONS=true`), la busqueda de repositorio siempre esta activa. + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/git-introduction.md b/tools/gitpagedocs/docs/versions/1.1.0/es/git-introduction.md new file mode 100644 index 0000000..f99a5df --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/git-introduction.md @@ -0,0 +1,12 @@ +# Introduccion a Git + +Conceptos basicos de Git para principiantes. + +## Comandos esenciales + +- `git init` - iniciar repositorio +- `git add` - preparar cambios +- `git commit` - registrar commit +- `git push` - enviar a remoto + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.1.0/es/github-issues-projects.md new file mode 100644 index 0000000..2030bee --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/github-issues-projects.md @@ -0,0 +1,11 @@ +# GitHub Issues y Projects + +Aprende a usar GitHub Issues y Projects para gestionar tu trabajo. + +## Conceptos + +- Issues para rastrear tareas y bugs +- Projects para visualizar y organizar el trabajo +- Flujos recomendados para equipos + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/index.md b/tools/gitpagedocs/docs/versions/1.1.0/es/index.md new file mode 100644 index 0000000..8b27f26 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs es un runtime de documentacion multilenguaje para repositorios que incluyen la carpeta `gitpagedocs/`. + +## Que entrega este proyecto + +- Renderizado markdown multilenguaje (`en`, `pt`, `es`) +- Ruteo por version (`/v/:version`) +- Sistema de temas con templates JSON +- Ejecucion local y en GitHub Pages +- Busqueda de repositorio + render remoto opcional + +## Contrato de carpetas + +El runtime espera esta estructura: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Navegacion rapida + +- Abre **Primeros pasos** para setup local. +- Abre **Configuracion** para detalle completo de `config.json`. +- Abre **Publicacion** para comportamiento local/produccion/GitHub Pages. +- Abre **Arquitectura** para mapa de codigo y flujo de datos. +- Abre **Temas y layouts** para creacion de templates. +- Abre **Rutas autorizadas** para configurar clave, roles y autenticacion externa. +- Abre **FAQ** para troubleshooting. + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/project-overview.md b/tools/gitpagedocs/docs/versions/1.1.0/es/project-overview.md new file mode 100644 index 0000000..db39de3 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/project-overview.md @@ -0,0 +1,27 @@ +# Vision general del proyecto + +Git Page Docs es un monorepo pnpm + turborepo que convierte la carpeta `gitpagedocs/` de un repositorio en un sitio de documentacion multilingue y versionado — con un asistente de IA integrado y un servidor MCP (Model Context Protocol). + +## Paquetes del monorepo + +- **frontend/** — visor Next.js 15 (App Router, React 19), exportado estaticamente para GitHub Pages. +- **cli/** — el paquete npm publicado `gitpagedocs` (`npm install -g gitpagedocs`): genera la estructura de docs, documenta con IA, configura Pages y ejecuta el servidor MCP. +- **tools/** — `@gitpagedocs/tools`, el nucleo de logica compartido: sistema de IA con 14 proveedores, boveda de credenciales cifrada, cargador de config, caches y logger. +- **mcp/** — `@gitpagedocs/mcp`, servidor Model Context Protocol (20 herramientas + 7 recursos). +- **gitpagedocs/** — el contrato del usuario: `config.json`, docs versionados y layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; exportacion estatica para GitHub Pages +- gray-matter + marked para Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Destacados + +- Multilingue (`en`, `pt`, `es`) y rutas por version (`/v/:version`) +- Sistema de IA con 14 proveedores (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI y mas) con streaming +- Claves de IA **cifradas en reposo** (AES-256-GCM) detras de una contrasena local — nunca en texto plano +- **Panel de chat de IA** en los docs + una **consola `/ai`** dedicada +- Sistema de 36 temas; ejecucion local y en GitHub Pages + +> Version (ES): 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/source-viewer b/tools/gitpagedocs/docs/versions/1.1.0/es/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.0/es/source-viewer.html b/tools/gitpagedocs/docs/versions/1.1.0/es/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/es/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/authorized-routes.md new file mode 100644 index 0000000..0ea8a7c --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/authorized-routes.md @@ -0,0 +1,55 @@ +# Rotas autorizadas + +Proteja rotas por chave de acesso, papeis obrigatorios e provedores externos. + +## Local do config de versao + +Configure em: + +- `gitpagedocs/docs/versions//config.json` + +## Secao global auth + +Use `auth` no topo do config de versao: + +- `accessKeys`: mapa de ids de chave para segredo esperado +- `rolesStorageKey`: chave de localStorage para bootstrap de papeis +- `providers`: lista de provedores externos (`authjs`, `clerk`, `firebase`, `jwt`) + +## Autorizacao por rota + +Dentro de cada rota (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Fases + +### Fase A - Chave de acesso + +Defina `authorization.accessKeyId` e a chave correspondente em `auth.accessKeys`. + +### Fase B - Papeis + +Defina `authorization.requiredRoles` com um ou mais papeis. + +Os papeis podem vir de: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- claims de provedores externos + +### Fase C - Provedores externos + +Defina `authorization.requireExternalAuth=true` e opcionalmente `allowedProviders`. + +Adaptadores suportados: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- JWT custom (`type: "jwt"`) + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/functionalities.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/functionalities.md new file mode 100644 index 0000000..60835e3 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/functionalities.md @@ -0,0 +1,68 @@ +# Funcionalidades + +Referencia completa de opcoes da CLI, chaves de configuracao e recursos do runtime. + +## Comandos da CLI + +| Comando | Descricao | +|---------|------------| +| `npx gitpagedocs` | Gera config e docs em `gitpagedocs/` | +| `npx gitpagedocs --layoutconfig` | Tambem gera layouts/templates locais | +| `npx gitpagedocs --home` | Distribuicao standalone (`gitpagedocshome/`) | +| `npx gitpagedocs --push --owner X --repo Y` | Configura workflow, commit, push | +| `npx gitpagedocs --interactive` / `-i` | Modo interativo com prompts | + +## Opcoes da CLI + +| Opcao | Descricao | +|-------|-----------| +| `--owner ` | Owner do GitHub | +| `--repo ` | Repositorio GitHub | +| `--path ` | Subcaminho dos docs (ex: `docs`); sem ele, base path = nome do repo para CSS/JS em project sites | +| `--output ` | Diretorio de saida (padrao: `gitpagedocs`) | +| `--search true|false` | Habilita/desabilita busca de repositorio (`--home`) | +| `--layoutconfig` | Gera `gitpagedocs/layouts/` | +| `--push` | Cria workflow, commit de artefatos, push | +| `--home` | Gera `gitpagedocshome/` (estatico + .env + Dockerfile) | + +## Saida gerada + +- `gitpagedocs/config.json` – config raiz +- `gitpagedocs/icon.svg` – icone padrao +- `gitpagedocs/docs/versions//config.json` – rotas por versao +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – docs em markdown +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – visualizador de codigo (estilo GitHub) +- `gitpagedocs/layouts/` – apenas com `--layoutconfig` + +## Tipos de conteudo + +| Tipo | Chave config | Descricao | +|------|--------------|-----------| +| Markdown | `routes-md` | Arquivos .md com `path` por idioma | +| HTML | `routes-html` | `path` (ex: source-viewer) ou `url` externa | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Visualizador de codigo fonte + +A CLI gera uma pagina **Codigo fonte** por versao. Escaneia `src/`, `cli/` e arquivos raiz (README.md, package.json, next.config.ts, etc.) e constroi um visualizador estilo GitHub em modo escuro com: + +- Arvore de arquivos na lateral com expandir/recolher pastas +- Filtro de busca +- Destaque de sintaxe (TypeScript, JavaScript, JSON, CSS, Markdown) +- Botao copiar, numeros de linha +- Alternar preview/codigo do README.md +- Controles Expandir tudo / Recolher tudo + +## Chaves de config (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Variaveis de ambiente + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – busca de repositorio (local) +- `GITHUB_ACTIONS` – modo build GitHub Pages + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/getting-started.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/getting-started.md new file mode 100644 index 0000000..c2dff9b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/getting-started.md @@ -0,0 +1,40 @@ +# Primeiros passos + +Este guia leva o projeto do zero ate docs rodando. + +## Pre-requisitos + +- Node.js 20+ +- npm 10+ (ou pnpm) + +## Setup local + +1. Instale dependencias: + - `npm install` +2. Gere/atualize os artefatos de docs: + - `npm run gitpagedocs` +3. Inicie o desenvolvimento: + - `npm run dev` +4. Build e execucao local de producao: + - `npm run build` + - `npm start` + +## Comportamento da CLI + +`npx gitpagedocs` (ou `npm run gitpagedocs`) gera os artefatos na pasta oficial `gitpagedocs/`. + +- Gera somente markdown/json +- Nao gera `index.html` +- Nao gera `index.js` +- Nao executa comandos de instalacao + +## Modo de busca por repositorio + +No ambiente local, o controle e por variavel: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +Em build de GitHub Pages (`GITHUB_ACTIONS=true`), a busca de repositorio fica sempre ativa. + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/git-introduction.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/git-introduction.md new file mode 100644 index 0000000..0ac49a4 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/git-introduction.md @@ -0,0 +1,12 @@ +# Introducao ao Git + +Conceitos basicos de Git para iniciantes. + +## Comandos essenciais + +- `git init` - iniciar repositorio +- `git add` - preparar alteracoes +- `git commit` - registrar commit +- `git push` - enviar para remoto + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/github-issues-projects.md new file mode 100644 index 0000000..33bacde --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/github-issues-projects.md @@ -0,0 +1,11 @@ +# GitHub Issues e Projects + +Aprenda a usar GitHub Issues e Projects para gerenciar seu trabalho. + +## Conceitos + +- Issues para rastrear tarefas e bugs +- Projects para visualizar e organizar o trabalho +- Workflows recomendados para equipes + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/index.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/index.md new file mode 100644 index 0000000..ea309a8 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs e um runtime de documentacao multi-idioma para repositorios que possuem a pasta `gitpagedocs/`. + +## O que este projeto entrega + +- Renderizacao markdown em varios idiomas (`en`, `pt`, `es`) +- Roteamento por versao (`/v/:versao`) +- Sistema de temas por templates JSON +- Execucao local e em GitHub Pages +- Busca de repositorio + renderizacao remota opcional + +## Contrato de pastas + +O runtime espera esta estrutura: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Navegacao rapida + +- Abra **Primeiros passos** para setup local. +- Abra **Configuracao** para detalhes completos do `config.json`. +- Abra **Publicacao** para comportamento local/producao/GitHub Pages. +- Abra **Arquitetura** para mapa de codigo e fluxo de dados. +- Abra **Temas e layouts** para autoria de templates. +- Abra **Rotas autorizadas** para configurar chave, papeis e autenticacao externa. +- Abra **FAQ** para troubleshooting. + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/project-overview.md b/tools/gitpagedocs/docs/versions/1.1.0/pt/project-overview.md new file mode 100644 index 0000000..636d83c --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/project-overview.md @@ -0,0 +1,27 @@ +# Visao geral do projeto + +Git Page Docs e um monorepo pnpm + turborepo que transforma a pasta `gitpagedocs/` de um repositorio em um site de documentacao multilinguagem e versionado — com assistente de IA integrado e um servidor MCP (Model Context Protocol). + +## Pacotes do monorepo + +- **frontend/** — visualizador Next.js 15 (App Router, React 19), exportado estaticamente para o GitHub Pages. +- **cli/** — o pacote npm publicado `gitpagedocs` (`npm install -g gitpagedocs`): gera a estrutura de docs, documenta com IA, configura o Pages e roda o servidor MCP. +- **tools/** — `@gitpagedocs/tools`, o nucleo de logica compartilhado: sistema de IA com 14 provedores, cofre de credenciais criptografado, loader de config, caches e logger. +- **mcp/** — `@gitpagedocs/mcp`, servidor Model Context Protocol (20 ferramentas + 7 recursos). +- **gitpagedocs/** — o contrato do usuario: `config.json`, docs versionados e layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; exportacao estatica para GitHub Pages +- gray-matter + marked para Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Destaques + +- Multilinguagem (`en`, `pt`, `es`) e rotas por versao (`/v/:version`) +- Sistema de IA com 14 provedores (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI e mais) com streaming +- Chaves de IA **criptografadas em repouso** (AES-256-GCM) atras de uma senha local — nunca em texto puro +- **Drawer de chat de IA** nos docs + um **console `/ai`** dedicado +- Sistema de 36 temas; execucao local e no GitHub Pages + +> Versao: 1.1.0 diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/source-viewer b/tools/gitpagedocs/docs/versions/1.1.0/pt/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.0/pt/source-viewer.html b/tools/gitpagedocs/docs/versions/1.1.0/pt/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.0/pt/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.1/config.json b/tools/gitpagedocs/docs/versions/1.1.1/config.json new file mode 100644 index 0000000..bd08492 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/config.json @@ -0,0 +1,716 @@ +{ + "auth": { + "accessKeys": { + "docs-key": "open-gitpagedocs-docs", + "source-viewer-key": "open-source-viewer" + }, + "rolesStorageKey": "git-page-docs:route-auth:roles", + "providers": [ + { + "type": "authjs", + "enabled": true, + "sessionEndpoint": "/api/auth/session", + "rolesClaimPath": "user.roles" + }, + { + "type": "clerk", + "enabled": true, + "rolesClaimPath": "claims.publicMetadata.roles" + }, + { + "type": "firebase", + "enabled": true, + "tokenStorageKey": "git-page-docs:firebase-token", + "rolesClaimPath": "roles" + }, + { + "type": "jwt", + "enabled": true, + "tokenStorageKey": "git-page-docs:jwt-token", + "rolesClaimPath": "roles" + } + ] + }, + "routes-md": [ + { + "id": 1, + "title": { + "pt": "Primeiros passos", + "en": "Getting Started", + "es": "Primeros pasos" + }, + "description": { + "pt": "Configure o repositório do zero", + "en": "Configure repository from zero", + "es": "Configura el repositorio desde cero" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/getting-started.md", + "en": "gitpagedocs/docs/versions/1.1.1/en/getting-started.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/getting-started.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false + }, + { + "id": 2, + "title": { + "pt": "Visão geral do projeto", + "en": "Project overview", + "es": "Visión general del proyecto" + }, + "description": { + "pt": "Stack, objetivo e estrutura do Git Page Docs", + "en": "Stack, goals and structure of Git Page Docs", + "es": "Stack, objetivos y estructura de Git Page Docs" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/project-overview.md", + "en": "gitpagedocs/docs/versions/1.1.1/en/project-overview.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/project-overview.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "audio": { + "enabled": true, + "autoPlayOnLoad": true, + "loopEnabled": false, + "tracks": [ + { + "url": "https://www.youtube.com/watch?v=0w80F8FffQ4", + "type": "youtube", + "title": { + "pt": "Música página 2", + "en": "Page 2 music", + "es": "Música página 2" + } + } + ] + } + }, + { + "id": 3, + "title": { + "pt": "Funcionalidades", + "en": "Functionalities", + "es": "Funcionalidades" + }, + "description": { + "pt": "CLI, opções, configuração e recursos", + "en": "CLI, options, configuration and features", + "es": "CLI, opciones, configuración y funciones" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/functionalities.md", + "en": "gitpagedocs/docs/versions/1.1.1/en/functionalities.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/functionalities.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "authorization": { + "requiredRoles": [ + "maintainer" + ] + } + }, + { + "id": 4, + "title": { + "pt": "GitHub Issues e Projects", + "en": "GitHub issues and projects", + "es": "GitHub issues y projects" + }, + "description": { + "pt": "Como usar Issues e Projects no GitHub", + "en": "How to use GitHub Issues and Projects", + "es": "Cómo usar GitHub Issues y Projects" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/github-issues-projects.md", + "en": "gitpagedocs/docs/versions/1.1.1/en/github-issues-projects.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/github-issues-projects.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false + }, + { + "id": 5, + "title": { + "pt": "Introdução ao Git", + "en": "Introduction to Git", + "es": "Introducción a Git" + }, + "description": { + "pt": "Conceitos básicos de Git para iniciantes", + "en": "Basic Git concepts for beginners", + "es": "Conceptos básicos de Git para principiantes" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/git-introduction.md", + "en": "gitpagedocs/docs/versions/1.1.1/en/git-introduction.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/git-introduction.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false + }, + { + "id": 6, + "title": { + "pt": "Rotas autorizadas", + "en": "Authorized routes", + "es": "Rutas autorizadas" + }, + "description": { + "pt": "Controle acesso por chave, roles e provedores externos", + "en": "Control access by key, roles and external providers", + "es": "Controla acceso por clave, roles y proveedores externos" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md", + "en": "gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "authorization": { + "accessKeyId": "docs-key", + "requiredRoles": [ + "maintainer" + ], + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + } + ], + "routes-html": [ + { + "id": 1, + "title": { + "pt": "Código fonte", + "en": "Source code", + "es": "Código fuente" + }, + "description": { + "pt": "Visualizar código-fonte do projeto", + "en": "View project source code", + "es": "Ver código fuente del proyecto" + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "path": { + "pt": "gitpagedocs/docs/versions/1.1.1/pt/source-viewer", + "en": "gitpagedocs/docs/versions/1.1.1/en/source-viewer", + "es": "gitpagedocs/docs/versions/1.1.1/es/source-viewer" + }, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "container": "full", + "browseAll": false, + "RouteguideBrand": true, + "RouteGuideSpeciFicbrand": [], + "RouteguideBrandPosition": "center", + "RouteguideBrandContainerTop": false, + "authorization": { + "accessKeyId": "source-viewer-key" + } + } + ], + "routes-video": [ + { + "id": 1, + "title": { + "pt": "Interactive vs non-interactive modes | Copilot CLI for beginners", + "en": "Interactive vs non-interactive modes | Copilot CLI for beginners", + "es": "Interactive vs non-interactive modes | Copilot CLI for beginners" + }, + "description": { + "pt": "Quer saber a forma mais rápida de usar o GitHub Copilot no terminal? Neste tutorial, exploramos os dois modos principais do Copilot CLI.", + "en": "Want to know the fastest way to prompt GitHub Copilot from your terminal? In this beginner tutorial, we explore the two main modes of the Copilot CLI. Discover how to use the interactive mode to have GitHub Copilot run your project locally or use the non-interactive mode with the -p flag for quick summaries without leaving your shell context.", + "es": "¿Quieres conocer la forma más rápida de usar GitHub Copilot desde tu terminal? En este tutorial exploramos los dos modos principales del Copilot CLI." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "bdIJkGr2NV0", + "en": "bdIJkGr2NV0", + "es": "bdIJkGr2NV0" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + }, + { + "id": 2, + "title": { + "pt": "How to use GitHub issues and projects | GitHub for Beginners", + "en": "How to use GitHub issues and projects | GitHub for Beginners", + "es": "How to use GitHub issues and projects | GitHub for Beginners" + }, + "description": { + "pt": "Aprenda a usar GitHub Issues e Projects para gerenciar seu trabalho.", + "en": "Learn how to use GitHub Issues and Projects to manage your work effectively.", + "es": "Aprende a usar GitHub Issues y Projects para gestionar tu trabajo." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "c67GaAkf1BE", + "en": "c67GaAkf1BE", + "es": "c67GaAkf1BE" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + }, + { + "id": 3, + "title": { + "pt": "How I built an AI Python tutor with the GitHub Copilot SDK", + "en": "How I built an AI Python tutor with the GitHub Copilot SDK", + "es": "How I built an AI Python tutor with the GitHub Copilot SDK" + }, + "description": { + "pt": "Construindo um tutor de Python com IA usando o GitHub Copilot SDK.", + "en": "Building an AI Python tutor using the GitHub Copilot SDK.", + "es": "Construyendo un tutor de Python con IA usando el GitHub Copilot SDK." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "N3my6W_Rdwg", + "en": "N3my6W_Rdwg", + "es": "N3my6W_Rdwg" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + }, + { + "id": 4, + "title": { + "pt": "A brief introduction to Git for beginners | GitHub", + "en": "A brief introduction to Git for beginners | GitHub", + "es": "A brief introduction to Git for beginners | GitHub" + }, + "description": { + "pt": "Introdução ao Git para iniciantes.", + "en": "A brief introduction to Git for beginners.", + "es": "Una breve introducción a Git para principiantes." + }, + "titleCss": "font-size: 1.85rem; font-weight: 700;", + "titleDarkCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titleLightCss": "font-size: 1.85rem; font-weight: 700; color: var(--text);", + "titlePosition": "center", + "titleIsVisible": true, + "descriptionCss": "font-size: 1.2rem; font-weight: 500;", + "descriptionDarkCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionLightCss": "font-size: 1.2rem; font-weight: 500; color: var(--text-secondary);", + "descriptionPosition": "center", + "descriptionIsVisible": true, + "fullscreenEnabled": true, + "marginTop": "", + "marginBottom": "", + "blockLink": true, + "browseAll": false, + "video": { + "videoType": { + "pt": "youtube", + "en": "youtube", + "es": "youtube" + }, + "pathVideo": { + "pt": "r8jQ9hVA2qs", + "en": "r8jQ9hVA2qs", + "es": "r8jQ9hVA2qs" + } + }, + "authorization": { + "requireExternalAuth": true, + "allowedProviders": [ + "authjs", + "clerk", + "firebase", + "jwt" + ] + } + } + ], + "menus-header-md": [ + { + "id": 1, + "pt": { + "title": "Primeiros passos", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/getting-started.md" + }, + "en": { + "title": "Getting Started", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/getting-started.md" + }, + "es": { + "title": "Primeros pasos", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/getting-started.md" + } + }, + { + "id": 2, + "pt": { + "title": "Visão geral do projeto", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/project-overview.md" + }, + "en": { + "title": "Project overview", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/project-overview.md" + }, + "es": { + "title": "Visión general del proyecto", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/project-overview.md" + } + }, + { + "id": 3, + "pt": { + "title": "Funcionalidades", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/functionalities.md" + }, + "en": { + "title": "Functionalities", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/functionalities.md" + }, + "es": { + "title": "Funcionalidades", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/functionalities.md" + } + }, + { + "id": 4, + "pt": { + "title": "GitHub Issues e Projects", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/github-issues-projects.md" + }, + "en": { + "title": "GitHub issues and projects", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/github-issues-projects.md" + }, + "es": { + "title": "GitHub issues y projects", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/github-issues-projects.md" + } + }, + { + "id": 5, + "pt": { + "title": "Introdução ao Git", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/git-introduction.md" + }, + "en": { + "title": "Introduction to Git", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/git-introduction.md" + }, + "es": { + "title": "Introducción a Git", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/git-introduction.md" + } + }, + { + "id": 6, + "pt": { + "title": "Rotas autorizadas", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md" + }, + "en": { + "title": "Authorized routes", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md" + }, + "es": { + "title": "Rutas autorizadas", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md" + } + } + ], + "menus-header-html": [ + { + "id": 1, + "pt": { + "title": "Código fonte", + "path-click": "gitpagedocs/docs/versions/1.1.1/pt/source-viewer" + }, + "en": { + "title": "Source code", + "path-click": "gitpagedocs/docs/versions/1.1.1/en/source-viewer" + }, + "es": { + "title": "Código fuente", + "path-click": "gitpagedocs/docs/versions/1.1.1/es/source-viewer" + } + } + ], + "menus-header-video": [ + { + "id": 1, + "pt": { + "title": "Interactive vs non-interactive modes | C...", + "path-click": "page:1" + }, + "en": { + "title": "Interactive vs non-interactive modes | C...", + "path-click": "page:1" + }, + "es": { + "title": "Interactive vs non-interactive modes | C...", + "path-click": "page:1" + } + }, + { + "id": 2, + "pt": { + "title": "How to use GitHub issues and projects | ...", + "path-click": "page:2" + }, + "en": { + "title": "How to use GitHub issues and projects | ...", + "path-click": "page:2" + }, + "es": { + "title": "How to use GitHub issues and projects | ...", + "path-click": "page:2" + } + }, + { + "id": 3, + "pt": { + "title": "How I built an AI Python tutor with the ...", + "path-click": "page:3" + }, + "en": { + "title": "How I built an AI Python tutor with the ...", + "path-click": "page:3" + }, + "es": { + "title": "How I built an AI Python tutor with the ...", + "path-click": "page:3" + } + }, + { + "id": 4, + "pt": { + "title": "A brief introduction to Git for beginner...", + "path-click": "page:4" + }, + "en": { + "title": "A brief introduction to Git for beginner...", + "path-click": "page:4" + }, + "es": { + "title": "A brief introduction to Git for beginner...", + "path-click": "page:4" + } + } + ], + "hierarchyPage": { + "md": 0, + "html": 1, + "video": 2, + "audio": 3 + }, + "hierarchyMenu": { + "md": 0, + "html": 1, + "video": 2, + "audio": 3 + } +} diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md new file mode 100644 index 0000000..8fc0ae7 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md @@ -0,0 +1,87 @@ +# Authorized Routes + +Protect routes by access key, required roles, and external authentication providers. + +## Version config location + +Configure this at: + +- `gitpagedocs/docs/versions//config.json` + +## Global auth section + +Use top-level `auth` in version config: + +- `accessKeys`: map of key ids to expected secrets +- `rolesStorageKey`: localStorage key for role bootstrap +- `providers`: external providers list (`authjs`, `clerk`, `firebase`, `jwt`) + +## Route-level authorization + +Inside each route (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Phases + +### Phase A - Access key + +Set `authorization.accessKeyId` and define that key in `auth.accessKeys`. + +### Phase B - Roles + +Set `authorization.requiredRoles` with one or more roles. + +Roles can come from: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- external provider claims + +### Phase C - External providers + +Set `authorization.requireExternalAuth=true` and optionally `allowedProviders`. + +Supported adapters: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- Custom JWT (`type: "jwt"`) + +## Example + +```json +{ + "auth": { + "accessKeys": { + "docs-key": "open-gitpagedocs-docs" + }, + "providers": [ + { "type": "authjs", "enabled": true, "sessionEndpoint": "/api/auth/session" }, + { "type": "jwt", "enabled": true, "tokenStorageKey": "git-page-docs:jwt-token" } + ] + }, + "routes-md": [ + { + "id": 6, + "path": { + "en": "gitpagedocs/docs/versions/1.1.1/en/authorized-routes.md", + "pt": "gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md", + "es": "gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md" + }, + "authorization": { + "accessKeyId": "docs-key", + "requiredRoles": ["maintainer"], + "requireExternalAuth": true, + "allowedProviders": ["authjs", "jwt"] + } + } + ] +} +``` + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/functionalities.md b/tools/gitpagedocs/docs/versions/1.1.1/en/functionalities.md new file mode 100644 index 0000000..6d4da38 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/functionalities.md @@ -0,0 +1,90 @@ +# Functionalities + +Complete reference of CLI options, configuration keys, and runtime features. + +## CLI commands + +| Command | Description | +|---------|--------------| +| `gitpagedocs` | Generate config and docs in `gitpagedocs/` | +| `gitpagedocs --layoutconfig` | Also generate local layouts/templates | +| `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`) | +| `gitpagedocs --push --owner X --repo Y` | Setup workflow, commit, push | +| `gitpagedocs --interactive` / `-i` | Interactive mode with prompts | +| `gitpagedocs ai` | Interactive AI documentation generator | +| `gitpagedocs provider [id]` / `models [provider]` | List AI providers / catalog models | +| `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI | +| `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push | +| `gitpagedocs docs` | Refresh README/CONTRIBUTING/SECURITY managed regions | +| `gitpagedocs doctor` / `version` / `update` | Diagnostics / version / update hint | +| `gitpagedocs mcp start` | Start the MCP server over stdio | + +Install globally with `npm install -g gitpagedocs`, or run one-off with `npx gitpagedocs`. + +## CLI options + +| Option | Description | +|--------|-------------| +| `--owner ` | GitHub owner | +| `--repo ` | GitHub repository | +| `--path ` | Docs subpath (e.g. `docs`); without it, base path = repo name for correct CSS/JS on project sites | +| `--output ` | Output directory (default: `gitpagedocs`) | +| `--search true|false` | Enable/disable repository search (`--home`) | +| `--layoutconfig` | Generate `gitpagedocs/layouts/` | +| `--push` | Create workflow, commit artifacts, push | +| `--home` | Generate `gitpagedocshome/` (static + .env + Dockerfile) | + +## Generated output + +- `gitpagedocs/config.json` – root config +- `gitpagedocs/icon.svg` – default icon +- `gitpagedocs/docs/versions//config.json` – per-version routes +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – markdown docs +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – GitHub-style source code viewer +- `gitpagedocs/layouts/` – only with `--layoutconfig` + +## Content types + +| Type | Config key | Description | +|------|------------|-------------| +| Markdown | `routes-md` | .md files with `path` per language | +| HTML | `routes-html` | `path` (e.g. source-viewer) or `url` for external | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Source code viewer + +The CLI generates a **Source code** page per version. It scans `src/`, `cli/`, and root files (README.md, package.json, next.config.ts, etc.) and builds a GitHub-style dark viewer with: + +- File tree sidebar with folder collapse/expand +- Search filter for files +- Syntax highlighting (TypeScript, JavaScript, JSON, CSS, Markdown) +- Copy button, line numbers +- README.md preview/code toggle +- Expand all / Collapse all controls + +## Config keys (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Environment variables + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – repository search (local) +- `GITHUB_ACTIONS` – GitHub Pages build mode + +## AI assistant + +The docs ship an AI assistant in two surfaces: an in-docs **chat drawer** (the ✨ button in the sidebar, enabled via `site.AiChatEnabled`) and a dedicated **`/ai` console** page. + +- **14 providers** via one shared core: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity. +- **Model selection** — pick from each provider's catalog (`gitpagedocs models `) or type a custom id. +- **Encrypted at rest** — your API key is sealed with AES-256-GCM behind a **local password** (one unlock per session) and is never stored in plaintext or logged. A legacy plaintext key is migrated and wiped on first unlock. +- **AI documentation generation** — `gitpagedocs ai` scans chosen paths and writes multilingual markdown (pt/en/es); reusable via `.gitpagedocsconfig`. + +## MCP server + +`gitpagedocs mcp start` runs a Model Context Protocol server (stdio) exposing **20 tools** (filesystem, AI, doc generation/analysis) and **7 resources** (`project://structure|docs|config|repository|readme|ai/providers|ai/models`) for editors and AI agents. + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/getting-started.md b/tools/gitpagedocs/docs/versions/1.1.1/en/getting-started.md new file mode 100644 index 0000000..feb5fd3 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/getting-started.md @@ -0,0 +1,45 @@ +# Getting Started + +This guide configures your repository from zero to running docs. + +## Prerequisites + +- Node.js 20+ +- npm 10+ + +## Install and generate + +1. Install package: + - `npm install gitpagedocs` +2. Generate docs config and versions: + - `npx gitpagedocs` +3. Optional: generate local layouts/templates: + - `npx gitpagedocs --layoutconfig` + +## Local run + +1. Development: + - `npm run dev` +2. Production locally: + - `npm run build` + - `npm start` + +## CLI behavior + +`npx gitpagedocs` generates only artifacts in `gitpagedocs/`: + +- JSON + markdown docs assets +- No `index.html` +- No `index.js` +- No install command execution + +## Repository search mode + +Local repository search is controlled by: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +On GitHub Pages builds (`GITHUB_ACTIONS=true`), repository-search home is enabled. + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/git-introduction.md b/tools/gitpagedocs/docs/versions/1.1.1/en/git-introduction.md new file mode 100644 index 0000000..80dd0d6 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/git-introduction.md @@ -0,0 +1,19 @@ +# Introduction to Git + +Basic Git concepts for beginners. + +## Key concepts + +- **Repository**: A project folder tracked by Git +- **Commit**: A snapshot of changes +- **Branch**: Alternative line of development +- **Remote**: Shared repository (e.g. on GitHub) + +## Common commands + +- `git init` - Initialize a repo +- `git add` - Stage changes +- `git commit` - Create a snapshot +- `git push` - Send to remote + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.1.1/en/github-issues-projects.md new file mode 100644 index 0000000..08a635b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/github-issues-projects.md @@ -0,0 +1,17 @@ +# GitHub Issues and Projects + +Learn how to use GitHub Issues and Projects to manage your work. + +## Issues + +- Track bugs, features, and tasks +- Assignees, labels, milestones +- Discussions and linked PRs + +## Projects + +- Kanban boards +- Tables and roadmaps +- Custom fields and automation + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/index.md b/tools/gitpagedocs/docs/versions/1.1.1/en/index.md new file mode 100644 index 0000000..77ff10e --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs is a multilingual documentation runtime for repositories that ship a `gitpagedocs/` folder. + +## What this project delivers + +- Multilingual markdown rendering (`en`, `pt`, `es`) +- Version-aware docs routing (`/v/:version`) +- Theme system with JSON templates +- Local and GitHub Pages execution modes +- Optional repository search + remote rendering + +## Folder contract + +The runtime expects this structure: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Quick navigation + +- Open **Getting Started** for local setup. +- Open **Configuration** for full `config.json` explanation. +- Open **Deployment** for local, server, and GitHub Pages behavior. +- Open **Architecture** for code map and data flow. +- Open **Themes and layouts** for template authoring details. +- Open **Authorized routes** for key, roles, and external auth setup. +- Open **FAQ** for troubleshooting. + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/project-overview.md b/tools/gitpagedocs/docs/versions/1.1.1/en/project-overview.md new file mode 100644 index 0000000..bdf2b1b --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/project-overview.md @@ -0,0 +1,27 @@ +# Project Overview + +Git Page Docs is a **pnpm + turborepo monorepo** that turns a repository's `gitpagedocs/` folder into a multilingual, versioned documentation site — with an integrated AI assistant and a Model Context Protocol (MCP) server. + +## Monorepo packages + +- **frontend/** — Next.js 15 (App Router, React 19) documentation viewer, static-exported for GitHub Pages. +- **cli/** — the published `gitpagedocs` npm package (`npm install -g gitpagedocs`): scaffolds the docs contract, generates docs with AI, configures Pages, and runs the MCP server. +- **tools/** — `@gitpagedocs/tools`, the shared business-logic core: the 14-provider AI system, the encrypted credential vault, the config loader, caches, and the logger. +- **mcp/** — `@gitpagedocs/mcp`, a Model Context Protocol server (20 tools + 7 resources). +- **gitpagedocs/** — the user contract: `config.json`, versioned docs, and layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; static export for GitHub Pages +- gray-matter + marked for Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Highlights + +- Multilingual (`en`, `pt`, `es`) and version-aware routing (`/v/:version`) +- 14-provider AI system (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI, and more) with streaming +- AI API keys stored **encrypted at rest** (AES-256-GCM) behind a local password gate — never plaintext +- In-docs **AI chat drawer** plus a dedicated **`/ai` console** +- 36-theme layout system; local and GitHub Pages execution modes + +> Version: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/source-viewer b/tools/gitpagedocs/docs/versions/1.1.1/en/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.1/en/source-viewer.html b/tools/gitpagedocs/docs/versions/1.1.1/en/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/en/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md new file mode 100644 index 0000000..de51828 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/authorized-routes.md @@ -0,0 +1,55 @@ +# Rutas autorizadas + +Protege rutas por clave de acceso, roles requeridos y proveedores externos. + +## Ubicacion del config de version + +Configura en: + +- `gitpagedocs/docs/versions//config.json` + +## Seccion global auth + +Usa `auth` en la raiz del config de version: + +- `accessKeys`: mapa de ids de clave al secreto esperado +- `rolesStorageKey`: clave de localStorage para bootstrap de roles +- `providers`: lista de proveedores externos (`authjs`, `clerk`, `firebase`, `jwt`) + +## Autorizacion por ruta + +Dentro de cada ruta (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Fases + +### Fase A - Clave de acceso + +Define `authorization.accessKeyId` y la clave correspondiente en `auth.accessKeys`. + +### Fase B - Roles + +Define `authorization.requiredRoles` con uno o mas roles. + +Los roles pueden venir de: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- claims de proveedores externos + +### Fase C - Proveedores externos + +Define `authorization.requireExternalAuth=true` y opcionalmente `allowedProviders`. + +Adaptadores soportados: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- JWT custom (`type: "jwt"`) + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/functionalities.md b/tools/gitpagedocs/docs/versions/1.1.1/es/functionalities.md new file mode 100644 index 0000000..1fcd916 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/functionalities.md @@ -0,0 +1,68 @@ +# Funcionalidades + +Referencia completa de opciones CLI, claves de configuracion y funciones del runtime. + +## Comandos CLI + +| Comando | Descripcion | +|---------|-------------| +| `npx gitpagedocs` | Genera config y docs en `gitpagedocs/` | +| `npx gitpagedocs --layoutconfig` | Tambien genera layouts/templates locales | +| `npx gitpagedocs --home` | Distribucion standalone (`gitpagedocshome/`) | +| `npx gitpagedocs --push --owner X --repo Y` | Configura workflow, commit, push | +| `npx gitpagedocs --interactive` / `-i` | Modo interactivo con prompts | + +## Opciones CLI + +| Opcion | Descripcion | +|--------|-------------| +| `--owner ` | Owner de GitHub | +| `--repo ` | Repositorio GitHub | +| `--path ` | Subruta de docs (ej: `docs`); sin ella, base path = nombre del repo para CSS/JS en project sites | +| `--output ` | Directorio de salida (default: `gitpagedocs`) | +| `--search true|false` | Habilita/deshabilita busqueda de repositorio (`--home`) | +| `--layoutconfig` | Genera `gitpagedocs/layouts/` | +| `--push` | Crea workflow, commit de artefactos, push | +| `--home` | Genera `gitpagedocshome/` (estatico + .env + Dockerfile) | + +## Salida generada + +- `gitpagedocs/config.json` – config raiz +- `gitpagedocs/icon.svg` – icono por defecto +- `gitpagedocs/docs/versions//config.json` – rutas por version +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – docs en markdown +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – visor de codigo (estilo GitHub) +- `gitpagedocs/layouts/` – solo con `--layoutconfig` + +## Tipos de contenido + +| Tipo | Clave config | Descripcion | +|------|--------------|-------------| +| Markdown | `routes-md` | Archivos .md con `path` por idioma | +| HTML | `routes-html` | `path` (ej: source-viewer) o `url` externa | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Visor de codigo fuente + +La CLI genera una pagina **Codigo fuente** por version. Escanea `src/`, `cli/` y archivos raiz (README.md, package.json, next.config.ts, etc.) y construye un visor estilo GitHub en modo oscuro con: + +- Arbol de archivos en barra lateral con expandir/colapsar carpetas +- Filtro de busqueda +- Resaltado de sintaxis (TypeScript, JavaScript, JSON, CSS, Markdown) +- Boton copiar, numeros de linea +- Alternar vista previa/codigo del README.md +- Controles Expandir todo / Colapsar todo + +## Claves de config (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Variables de entorno + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – busqueda de repositorio (local) +- `GITHUB_ACTIONS` – modo build GitHub Pages + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/getting-started.md b/tools/gitpagedocs/docs/versions/1.1.1/es/getting-started.md new file mode 100644 index 0000000..964722c --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/getting-started.md @@ -0,0 +1,40 @@ +# Primeros pasos + +Esta guia lleva el proyecto desde cero hasta docs corriendo. + +## Requisitos + +- Node.js 20+ +- npm 10+ (o pnpm) + +## Setup local + +1. Instala dependencias: + - `npm install` +2. Genera/actualiza artefactos de docs: + - `npm run gitpagedocs` +3. Inicia desarrollo: + - `npm run dev` +4. Build + ejecucion local de produccion: + - `npm run build` + - `npm start` + +## Comportamiento de la CLI + +`npx gitpagedocs` (o `npm run gitpagedocs`) genera artefactos en la carpeta oficial `gitpagedocs/`. + +- Genera solo markdown/json +- No genera `index.html` +- No genera `index.js` +- No ejecuta comandos de instalacion + +## Modo de busqueda por repositorio + +En local, se controla por variable: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +En build de GitHub Pages (`GITHUB_ACTIONS=true`), la busqueda de repositorio siempre esta activa. + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/git-introduction.md b/tools/gitpagedocs/docs/versions/1.1.1/es/git-introduction.md new file mode 100644 index 0000000..05637c2 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/git-introduction.md @@ -0,0 +1,12 @@ +# Introduccion a Git + +Conceptos basicos de Git para principiantes. + +## Comandos esenciales + +- `git init` - iniciar repositorio +- `git add` - preparar cambios +- `git commit` - registrar commit +- `git push` - enviar a remoto + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.1.1/es/github-issues-projects.md new file mode 100644 index 0000000..877f90d --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/github-issues-projects.md @@ -0,0 +1,11 @@ +# GitHub Issues y Projects + +Aprende a usar GitHub Issues y Projects para gestionar tu trabajo. + +## Conceptos + +- Issues para rastrear tareas y bugs +- Projects para visualizar y organizar el trabajo +- Flujos recomendados para equipos + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/index.md b/tools/gitpagedocs/docs/versions/1.1.1/es/index.md new file mode 100644 index 0000000..367de14 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs es un runtime de documentacion multilenguaje para repositorios que incluyen la carpeta `gitpagedocs/`. + +## Que entrega este proyecto + +- Renderizado markdown multilenguaje (`en`, `pt`, `es`) +- Ruteo por version (`/v/:version`) +- Sistema de temas con templates JSON +- Ejecucion local y en GitHub Pages +- Busqueda de repositorio + render remoto opcional + +## Contrato de carpetas + +El runtime espera esta estructura: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Navegacion rapida + +- Abre **Primeros pasos** para setup local. +- Abre **Configuracion** para detalle completo de `config.json`. +- Abre **Publicacion** para comportamiento local/produccion/GitHub Pages. +- Abre **Arquitectura** para mapa de codigo y flujo de datos. +- Abre **Temas y layouts** para creacion de templates. +- Abre **Rutas autorizadas** para configurar clave, roles y autenticacion externa. +- Abre **FAQ** para troubleshooting. + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/project-overview.md b/tools/gitpagedocs/docs/versions/1.1.1/es/project-overview.md new file mode 100644 index 0000000..149018c --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/project-overview.md @@ -0,0 +1,27 @@ +# Vision general del proyecto + +Git Page Docs es un monorepo pnpm + turborepo que convierte la carpeta `gitpagedocs/` de un repositorio en un sitio de documentacion multilingue y versionado — con un asistente de IA integrado y un servidor MCP (Model Context Protocol). + +## Paquetes del monorepo + +- **frontend/** — visor Next.js 15 (App Router, React 19), exportado estaticamente para GitHub Pages. +- **cli/** — el paquete npm publicado `gitpagedocs` (`npm install -g gitpagedocs`): genera la estructura de docs, documenta con IA, configura Pages y ejecuta el servidor MCP. +- **tools/** — `@gitpagedocs/tools`, el nucleo de logica compartido: sistema de IA con 14 proveedores, boveda de credenciales cifrada, cargador de config, caches y logger. +- **mcp/** — `@gitpagedocs/mcp`, servidor Model Context Protocol (20 herramientas + 7 recursos). +- **gitpagedocs/** — el contrato del usuario: `config.json`, docs versionados y layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; exportacion estatica para GitHub Pages +- gray-matter + marked para Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Destacados + +- Multilingue (`en`, `pt`, `es`) y rutas por version (`/v/:version`) +- Sistema de IA con 14 proveedores (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI y mas) con streaming +- Claves de IA **cifradas en reposo** (AES-256-GCM) detras de una contrasena local — nunca en texto plano +- **Panel de chat de IA** en los docs + una **consola `/ai`** dedicada +- Sistema de 36 temas; ejecucion local y en GitHub Pages + +> Version (ES): 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/source-viewer b/tools/gitpagedocs/docs/versions/1.1.1/es/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.1/es/source-viewer.html b/tools/gitpagedocs/docs/versions/1.1.1/es/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/es/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md new file mode 100644 index 0000000..f9f9fcb --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/authorized-routes.md @@ -0,0 +1,55 @@ +# Rotas autorizadas + +Proteja rotas por chave de acesso, papeis obrigatorios e provedores externos. + +## Local do config de versao + +Configure em: + +- `gitpagedocs/docs/versions//config.json` + +## Secao global auth + +Use `auth` no topo do config de versao: + +- `accessKeys`: mapa de ids de chave para segredo esperado +- `rolesStorageKey`: chave de localStorage para bootstrap de papeis +- `providers`: lista de provedores externos (`authjs`, `clerk`, `firebase`, `jwt`) + +## Autorizacao por rota + +Dentro de cada rota (`routes-md`, `routes-html`, `routes-video`): + +- `authorization.accessKeyId` +- `authorization.requiredRoles` +- `authorization.requireExternalAuth` +- `authorization.allowedProviders` + +## Fases + +### Fase A - Chave de acesso + +Defina `authorization.accessKeyId` e a chave correspondente em `auth.accessKeys`. + +### Fase B - Papeis + +Defina `authorization.requiredRoles` com um ou mais papeis. + +Os papeis podem vir de: + +- query param `?authRoles=admin,maintainer` +- localStorage (`rolesStorageKey`) +- claims de provedores externos + +### Fase C - Provedores externos + +Defina `authorization.requireExternalAuth=true` e opcionalmente `allowedProviders`. + +Adaptadores suportados: + +- Auth.js (`type: "authjs"`) +- Clerk (`type: "clerk"`) +- Firebase Auth (`type: "firebase"`) +- JWT custom (`type: "jwt"`) + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/functionalities.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/functionalities.md new file mode 100644 index 0000000..4b0d368 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/functionalities.md @@ -0,0 +1,68 @@ +# Funcionalidades + +Referencia completa de opcoes da CLI, chaves de configuracao e recursos do runtime. + +## Comandos da CLI + +| Comando | Descricao | +|---------|------------| +| `npx gitpagedocs` | Gera config e docs em `gitpagedocs/` | +| `npx gitpagedocs --layoutconfig` | Tambem gera layouts/templates locais | +| `npx gitpagedocs --home` | Distribuicao standalone (`gitpagedocshome/`) | +| `npx gitpagedocs --push --owner X --repo Y` | Configura workflow, commit, push | +| `npx gitpagedocs --interactive` / `-i` | Modo interativo com prompts | + +## Opcoes da CLI + +| Opcao | Descricao | +|-------|-----------| +| `--owner ` | Owner do GitHub | +| `--repo ` | Repositorio GitHub | +| `--path ` | Subcaminho dos docs (ex: `docs`); sem ele, base path = nome do repo para CSS/JS em project sites | +| `--output ` | Diretorio de saida (padrao: `gitpagedocs`) | +| `--search true|false` | Habilita/desabilita busca de repositorio (`--home`) | +| `--layoutconfig` | Gera `gitpagedocs/layouts/` | +| `--push` | Cria workflow, commit de artefatos, push | +| `--home` | Gera `gitpagedocshome/` (estatico + .env + Dockerfile) | + +## Saida gerada + +- `gitpagedocs/config.json` – config raiz +- `gitpagedocs/icon.svg` – icone padrao +- `gitpagedocs/docs/versions//config.json` – rotas por versao +- `gitpagedocs/docs/versions//{en,pt,es}/*.md` – docs em markdown +- `gitpagedocs/docs/versions//{en,pt,es}/source-viewer` – visualizador de codigo (estilo GitHub) +- `gitpagedocs/layouts/` – apenas com `--layoutconfig` + +## Tipos de conteudo + +| Tipo | Chave config | Descricao | +|------|--------------|-----------| +| Markdown | `routes-md` | Arquivos .md com `path` por idioma | +| HTML | `routes-html` | `path` (ex: source-viewer) ou `url` externa | +| Video | `routes-video` | `video.pathVideo`, `video.videoType` | +| Audio | `routes-audio` | `audio.pathAudio`, `audio.audioType` | + +## Visualizador de codigo fonte + +A CLI gera uma pagina **Codigo fonte** por versao. Escaneia `src/`, `cli/` e arquivos raiz (README.md, package.json, next.config.ts, etc.) e constroi um visualizador estilo GitHub em modo escuro com: + +- Arvore de arquivos na lateral com expandir/recolher pastas +- Filtro de busca +- Destaque de sintaxe (TypeScript, JavaScript, JSON, CSS, Markdown) +- Botao copiar, numeros de linha +- Alternar preview/codigo do README.md +- Controles Expandir tudo / Recolher tudo + +## Chaves de config (site) + +- `name`, `defaultLanguage`, `supportedLanguages` +- `docsVersion`, `rendering`, `ThemeDefault`, `ThemeModeDefault` +- `ProjectLink`, `layoutsConfigPathOficial`, `layoutsConfigPath` + +## Variaveis de ambiente + +- `GITPAGEDOCS_REPOSITORY_SEARCH` – busca de repositorio (local) +- `GITHUB_ACTIONS` – modo build GitHub Pages + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/getting-started.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/getting-started.md new file mode 100644 index 0000000..5f662dd --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/getting-started.md @@ -0,0 +1,40 @@ +# Primeiros passos + +Este guia leva o projeto do zero ate docs rodando. + +## Pre-requisitos + +- Node.js 20+ +- npm 10+ (ou pnpm) + +## Setup local + +1. Instale dependencias: + - `npm install` +2. Gere/atualize os artefatos de docs: + - `npm run gitpagedocs` +3. Inicie o desenvolvimento: + - `npm run dev` +4. Build e execucao local de producao: + - `npm run build` + - `npm start` + +## Comportamento da CLI + +`npx gitpagedocs` (ou `npm run gitpagedocs`) gera os artefatos na pasta oficial `gitpagedocs/`. + +- Gera somente markdown/json +- Nao gera `index.html` +- Nao gera `index.js` +- Nao executa comandos de instalacao + +## Modo de busca por repositorio + +No ambiente local, o controle e por variavel: + +- `GITPAGEDOCS_REPOSITORY_SEARCH=true` +- `GITPAGEDOCS_REPOSITORY_SEARCH=false` + +Em build de GitHub Pages (`GITHUB_ACTIONS=true`), a busca de repositorio fica sempre ativa. + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/git-introduction.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/git-introduction.md new file mode 100644 index 0000000..7289303 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/git-introduction.md @@ -0,0 +1,12 @@ +# Introducao ao Git + +Conceitos basicos de Git para iniciantes. + +## Comandos essenciais + +- `git init` - iniciar repositorio +- `git add` - preparar alteracoes +- `git commit` - registrar commit +- `git push` - enviar para remoto + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/github-issues-projects.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/github-issues-projects.md new file mode 100644 index 0000000..8fdf15f --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/github-issues-projects.md @@ -0,0 +1,11 @@ +# GitHub Issues e Projects + +Aprenda a usar GitHub Issues e Projects para gerenciar seu trabalho. + +## Conceitos + +- Issues para rastrear tarefas e bugs +- Projects para visualizar e organizar o trabalho +- Workflows recomendados para equipes + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/index.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/index.md new file mode 100644 index 0000000..64befb1 --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/index.md @@ -0,0 +1,34 @@ +# Git Page Docs + +Git Page Docs e um runtime de documentacao multi-idioma para repositorios que possuem a pasta `gitpagedocs/`. + +## O que este projeto entrega + +- Renderizacao markdown em varios idiomas (`en`, `pt`, `es`) +- Roteamento por versao (`/v/:versao`) +- Sistema de temas por templates JSON +- Execucao local e em GitHub Pages +- Busca de repositorio + renderizacao remota opcional + +## Contrato de pastas + +O runtime espera esta estrutura: + +- `gitpagedocs/config.json` +- `gitpagedocs/docs//*.md` +- `gitpagedocs/docs/versions//config.json` +- `gitpagedocs/docs/versions///*.md` +- `gitpagedocs/layouts/layoutsConfig.json` +- `gitpagedocs/layouts/templates/*.json` + +## Navegacao rapida + +- Abra **Primeiros passos** para setup local. +- Abra **Configuracao** para detalhes completos do `config.json`. +- Abra **Publicacao** para comportamento local/producao/GitHub Pages. +- Abra **Arquitetura** para mapa de codigo e fluxo de dados. +- Abra **Temas e layouts** para autoria de templates. +- Abra **Rotas autorizadas** para configurar chave, papeis e autenticacao externa. +- Abra **FAQ** para troubleshooting. + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/project-overview.md b/tools/gitpagedocs/docs/versions/1.1.1/pt/project-overview.md new file mode 100644 index 0000000..69dca4e --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/project-overview.md @@ -0,0 +1,27 @@ +# Visao geral do projeto + +Git Page Docs e um monorepo pnpm + turborepo que transforma a pasta `gitpagedocs/` de um repositorio em um site de documentacao multilinguagem e versionado — com assistente de IA integrado e um servidor MCP (Model Context Protocol). + +## Pacotes do monorepo + +- **frontend/** — visualizador Next.js 15 (App Router, React 19), exportado estaticamente para o GitHub Pages. +- **cli/** — o pacote npm publicado `gitpagedocs` (`npm install -g gitpagedocs`): gera a estrutura de docs, documenta com IA, configura o Pages e roda o servidor MCP. +- **tools/** — `@gitpagedocs/tools`, o nucleo de logica compartilhado: sistema de IA com 14 provedores, cofre de credenciais criptografado, loader de config, caches e logger. +- **mcp/** — `@gitpagedocs/mcp`, servidor Model Context Protocol (20 ferramentas + 7 recursos). +- **gitpagedocs/** — o contrato do usuario: `config.json`, docs versionados e layouts. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript; exportacao estatica para GitHub Pages +- gray-matter + marked para Markdown; react-icons +- pnpm workspaces + turborepo; Vitest + Playwright; ESLint + +## Destaques + +- Multilinguagem (`en`, `pt`, `es`) e rotas por versao (`/v/:version`) +- Sistema de IA com 14 provedores (OpenAI, Anthropic, Gemini, Ollama, Mistral, DeepSeek, Cohere, Groq, xAI e mais) com streaming +- Chaves de IA **criptografadas em repouso** (AES-256-GCM) atras de uma senha local — nunca em texto puro +- **Drawer de chat de IA** nos docs + um **console `/ai`** dedicado +- Sistema de 36 temas; execucao local e no GitHub Pages + +> Versao: 1.1.1 diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/source-viewer b/tools/gitpagedocs/docs/versions/1.1.1/pt/source-viewer new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/source-viewer @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/docs/versions/1.1.1/pt/source-viewer.html b/tools/gitpagedocs/docs/versions/1.1.1/pt/source-viewer.html new file mode 100644 index 0000000..d0e016a --- /dev/null +++ b/tools/gitpagedocs/docs/versions/1.1.1/pt/source-viewer.html @@ -0,0 +1,261 @@ + + + + + +Source code - GitHub + + + + + + + + + + + + +
+ source + / + - +
+
+ +
+ +
+
Select a file from the sidebar
+
+
+
+ + + + \ No newline at end of file diff --git a/tools/gitpagedocs/icon.svg b/tools/gitpagedocs/icon.svg new file mode 100644 index 0000000..3756fbe --- /dev/null +++ b/tools/gitpagedocs/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/tools/package.json b/tools/package.json index f2033e8..128f801 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,6 +1,6 @@ { "name": "@gitpagedocs/tools", - "version": "1.1.45", + "version": "1.1.53", "type": "module", "description": "Shared business-logic core for Git Page Docs (consumed by frontend, cli and mcp). Ships TypeScript source; consume via tsx or a TS-aware bundler.", "main": "./src/index.ts", diff --git a/tools/smoke/baseline.snapshot.json b/tools/smoke/baseline.snapshot.json index 428eb6f..410700b 100644 --- a/tools/smoke/baseline.snapshot.json +++ b/tools/smoke/baseline.snapshot.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-06-14T23:01:36.022Z", + "generatedAt": "2026-06-15T21:11:39.918Z", "targets": [ "gitpagedocs/config.json", "gitpagedocs/docs/versions/1.0.0/en/source-viewer", @@ -8,8 +8,8 @@ "gitpagedocs/docs/versions/1.1.1/config.json" ], "hashes": { - "gitpagedocs/config.json": "6285f31c944f528693c7b2620207fb0681bf85aedb80584dc0c4b8ea1ae6139d", - "gitpagedocs/docs/versions/1.0.0/en/source-viewer": "d07182090d1be5b48cf33536960c5971642ca1257b433cf90fba3602ca11dd74", + "gitpagedocs/config.json": "38bbb79615d5ddac15ec08e501745d521d2e7d9da29ce1f03d435906435492a6", + "gitpagedocs/docs/versions/1.0.0/en/source-viewer": "e4694d5e7c1de8245e91ccac231a50dee8986d784690d85fe41974c4dd9d5763", "gitpagedocs/docs/versions/1.0.0/config.json": "871779cf1e37f72110e414b47f6832cd14bff01bfb1aceeae9bf1b0f7f30d9b7", "gitpagedocs/docs/versions/1.1.0/config.json": "c6f5d0617216e964d44769f9abdecbaa2cc43ef4fb06e6003b87db8411d534aa", "gitpagedocs/docs/versions/1.1.1/config.json": "0dfea0e496c9cc55d3f94325497acc87a3c71d1cffddf24c1b16d0da3838d442" diff --git a/tools/src/crypto/doc-access.ts b/tools/src/crypto/doc-access.ts new file mode 100644 index 0000000..dd4d580 --- /dev/null +++ b/tools/src/crypto/doc-access.ts @@ -0,0 +1,62 @@ +/** + * Documentation password-gate key scheme (runtime-agnostic). + * + * Double-hash so config.json can ship a non-reversible verifier while the user + * keeps a copyable credential: + * privateKey = sha256(password) // printed by the CLI, shareable + * publicKey = sha256(privateKey) // stored in config.json + * Unlock succeeds when the supplied input is the password OR the private key. + * + * Pure: depends only on a { sha256 } service, so the SAME code runs in the CLI + * (NodeCryptoService) and the browser (WebCryptoService). It must NOT import + * node:crypto (e.g. safeHexEqual) so it stays safe to bundle for the web. + */ + +/** Minimal hashing surface — satisfied by both Node and Web CryptoService. */ +export interface Sha256Service { + sha256(input: string): Promise; +} + +export interface DocAccessKeys { + /** sha256(password) — printed for the user to copy/share. */ + readonly privateKey: string; + /** sha256(privateKey) — safe to commit in config.json. */ + readonly publicKey: string; +} + +/** Derive the { privateKey, publicKey } pair from a plaintext password. */ +export async function deriveDocAccessKeys( + password: string, + crypto: Sha256Service, +): Promise { + const privateKey = await crypto.sha256(password); + const publicKey = await crypto.sha256(privateKey); + return { privateKey, publicKey }; +} + +/** + * Verify a user-supplied credential (password OR private key) against the stored + * public key. Returns false when either side is empty. + */ +export async function verifyDocAccess( + input: string, + publicKey: string, + crypto: Sha256Service, +): Promise { + if (!input || !publicKey) return false; + const once = await crypto.sha256(input); + if (hexEqual(once, publicKey)) return true; // input is the private key + const twice = await crypto.sha256(once); + return hexEqual(twice, publicKey); // input is the password +} + +/** + * Length-checked, constant-time hex comparison. Local (not safeHexEqual) so this + * module never imports node:crypto and stays browser-bundle-safe. + */ +function hexEqual(a: string, b: string): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); + return diff === 0; +} diff --git a/tools/src/crypto/index.ts b/tools/src/crypto/index.ts index a540bbc..be2a3bf 100644 --- a/tools/src/crypto/index.ts +++ b/tools/src/crypto/index.ts @@ -1,2 +1,4 @@ export { NodeCryptoService, safeHexEqual } from "./node-crypto-service"; export { WebCryptoService } from "./web-crypto-service"; +export { deriveDocAccessKeys, verifyDocAccess } from "./doc-access"; +export type { DocAccessKeys, Sha256Service } from "./doc-access"; diff --git a/tools/src/crypto/web.ts b/tools/src/crypto/web.ts index 9a13ef3..316f79f 100644 --- a/tools/src/crypto/web.ts +++ b/tools/src/crypto/web.ts @@ -1,2 +1,4 @@ // Browser-safe crypto entry: Web Crypto only (no node:crypto). export { WebCryptoService } from "./web-crypto-service"; +export { deriveDocAccessKeys, verifyDocAccess } from "./doc-access"; +export type { DocAccessKeys, Sha256Service } from "./doc-access"; diff --git a/tools/src/documentation/sections.ts b/tools/src/documentation/sections.ts index a5776fc..28abb3b 100644 --- a/tools/src/documentation/sections.ts +++ b/tools/src/documentation/sections.ts @@ -73,7 +73,9 @@ export const CLI_COMMANDS: readonly CommandDoc[] = [ { name: "config", summary: "show the resolved gitpagedocs config" }, { name: "provider [id]", summary: "list AI providers or show one" }, { name: "models [provider]", summary: "list catalog models" }, - { name: "document[:repo|:file|:folder]", summary: "generate documentation with AI" }, + { name: "ai", summary: "interactive AI docs generator (writes pages in the gitpagedocs pattern)" }, + { name: "document[:repo|:file|:folder]", summary: "generate documentation with AI in the gitpagedocs pattern" }, + { name: "password", summary: "set a documentation access password (writes the public key to config.json)" }, { name: "deploy | pages", summary: "configure GitHub Pages via Actions and push" }, { name: "doctor", summary: "diagnose the environment" }, { name: "mcp start", summary: "start the MCP server over stdio" },