@gitpagedocs/cli 1.1.49 → 1.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,9 +21,10 @@ npx @gitpagedocs/cli
21
21
  | `gitpagedocs --push --owner <o> --repo <r> [--path <sub>]` | Generate, configure Pages, create workflow, commit + push |
22
22
  | `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`: static site + .env + Dockerfile) |
23
23
  | `gitpagedocs -i` / `--interactive` | Interactive prompts |
24
- | `gitpagedocs ai` | Interactive AI documentation generator (see below) |
24
+ | `gitpagedocs ai` | Interactive AI documentation generator — writes pages in the gitpagedocs pattern (see below) |
25
25
  | `gitpagedocs provider [id]` · `models [provider]` | List AI providers / catalog models |
26
- | `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI |
26
+ | `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI in the gitpagedocs pattern |
27
+ | `gitpagedocs password` | Set a documentation access password (writes the public key to `config.json`, prints the private key) |
27
28
  | `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push |
28
29
  | `gitpagedocs docs` | Refresh managed regions of README/CONTRIBUTING/SECURITY |
29
30
  | `gitpagedocs doctor` · `version` · `update` | Diagnostics / version / update hint |
@@ -134,9 +135,18 @@ npx gitpagedocs ai
134
135
  - asks provider (`openai`, `claude`, `gemini`, `ollama`)
135
136
  - asks API key or Ollama URL
136
137
  - asks paths to scan (supports one or many paths, including other repositories)
137
- - generates markdown documentation in `pt`, `en`, `es`
138
+ - generates documentation in `pt`, `en`, `es` **in the gitpagedocs pattern**
138
139
  - optionally persists config in `.gitpagedocsconfig`
139
- - optionally runs standard `gitpagedocs` scaffolding after AI generation
140
+
141
+ ### gitpagedocs pattern output
142
+
143
+ The model is given a gitpagedocs-aware system prompt and returns documentation split into
144
+ pages (delimited by `=== PAGE: <slug> | <Title> ===`). The CLI scaffolds the base
145
+ `gitpagedocs/` structure first, then writes each page to
146
+ `gitpagedocs/docs/versions/<latest>/<lang>/<slug>.md` for every language and **wires it into
147
+ that version's `config.json`** (`routes-md` + `menus-header-md`) so the pages render in the
148
+ viewer menu. Entries are tagged `aiGenerated` and idempotent (re-running replaces them). A
149
+ later plain `gitpagedocs` rebuild drops the wiring — re-run `gitpagedocs ai` to restore.
140
150
 
141
151
  ### `.gitpagedocsconfig` (manual mode)
142
152
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Parse an AI documentation response into gitpagedocs pages.
3
+ *
4
+ * The model is instructed (see GITPAGEDOCS_DOC_SYSTEM_PROMPT) to delimit each
5
+ * page with a line `=== PAGE: <slug> | <Title> ===`. This splits that response
6
+ * into individual pages. If the model ignored the format, the whole response is
7
+ * returned as a single "ai-overview" page so content is never lost.
8
+ */
9
+
10
+ export interface AiDocPage {
11
+ /** lowercase-kebab, English, identical across languages (used as the filename) */
12
+ slug: string;
13
+ title: string;
14
+ body: string;
15
+ }
16
+
17
+ const PAGE_DELIMITER = /^===\s*PAGE:\s*(.+?)\s*\|\s*(.*?)\s*===\s*$/;
18
+
19
+ /** Turn an arbitrary heading into a safe lowercase-kebab slug. */
20
+ export function slugify(raw: string): string {
21
+ const slug = (raw ?? "")
22
+ .toLowerCase()
23
+ .normalize("NFKD")
24
+ .replace(/[^a-z0-9]+/g, "-")
25
+ .replace(/^-+|-+$/g, "")
26
+ .slice(0, 60);
27
+ return slug || "page";
28
+ }
29
+
30
+ export function parseAiPages(markdown: string): AiDocPage[] {
31
+ const text = (markdown ?? "").replace(/\r\n/g, "\n").trim();
32
+ if (!text) return [];
33
+
34
+ const pages: AiDocPage[] = [];
35
+ let current: { slug: string; title: string; body: string[] } | null = null;
36
+
37
+ const flush = () => {
38
+ if (current) {
39
+ pages.push({ slug: current.slug, title: current.title, body: current.body.join("\n").trim() });
40
+ }
41
+ };
42
+
43
+ for (const line of text.split("\n")) {
44
+ const match = line.match(PAGE_DELIMITER);
45
+ if (match) {
46
+ flush();
47
+ const rawSlug = match[1].trim();
48
+ const title = match[2].trim() || rawSlug;
49
+ current = { slug: slugify(rawSlug), title, body: [] };
50
+ } else if (current) {
51
+ current.body.push(line);
52
+ }
53
+ }
54
+ flush();
55
+
56
+ const withBody = pages.filter((page) => page.body.length > 0);
57
+ if (withBody.length === 0) {
58
+ return [{ slug: "ai-overview", title: "Overview", body: text }];
59
+ }
60
+
61
+ // De-duplicate slugs within a single response (suffix later collisions).
62
+ const seen = new Map<string, number>();
63
+ return withBody.map((page) => {
64
+ const count = seen.get(page.slug) ?? 0;
65
+ seen.set(page.slug, count + 1);
66
+ return count === 0 ? page : { ...page, slug: `${page.slug}-${count + 1}` };
67
+ });
68
+ }
@@ -1,4 +1,3 @@
1
- import path from "node:path";
2
1
  import { AiCommandService } from "./ai-command";
3
2
  import { createAiProvider } from "./ai-provider-factory";
4
3
  import { FileSystemAdapter, type FilePayload } from "../infrastructure/file-system-adapter";
@@ -7,6 +6,11 @@ import {
7
6
  } from "../infrastructure/ai-config-file";
8
7
  import type { AiCliRunPlan, AiCliRunSummary } from "../core/models/ai-cli-config";
9
8
  import { promptMissingDirectories, runAiInteractivePrompt } from "../presentation/ai-prompts";
9
+ import { GITPAGEDOCS_DOC_SYSTEM_PROMPT } from "../config";
10
+ import { parseAiPages, type AiDocPage } from "./docs-pattern";
11
+ import { writeVersionDocs } from "../infrastructure/version-docs-writer";
12
+ // @ts-expect-error .mjs runtime module is type-less in this package.
13
+ import { DOC_VERSIONS } from "../../contracts/doc-versions.mjs";
10
14
 
11
15
  const LANGUAGE_TO_LABEL: Record<string, string> = {
12
16
  pt: "Portuguese",
@@ -22,7 +26,13 @@ function aggregateFilesPayload(files: FilePayload[]): string {
22
26
 
23
27
  function buildLanguagePrompt(basePrompt: string, language: "pt" | "en" | "es"): string {
24
28
  const languageLabel = LANGUAGE_TO_LABEL[language] ?? language;
25
- 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.`;
29
+ const authorGuidance = basePrompt?.trim() ? `Additional author guidance:\n${basePrompt.trim()}\n\n` : "";
30
+ return (
31
+ `${GITPAGEDOCS_DOC_SYSTEM_PROMPT}\n\n` +
32
+ authorGuidance +
33
+ `Output language: write every page Title and body 100% in ${languageLabel}. ` +
34
+ `Keep each page slug lowercase-kebab and in English so the languages line up.`
35
+ );
26
36
  }
27
37
 
28
38
  async function collectFilesWithInteractiveFallback(
@@ -66,7 +76,7 @@ async function collectFilesWithInteractiveFallback(
66
76
  };
67
77
  }
68
78
 
69
- async function runPlan(plan: AiCliRunPlan): Promise<AiCliRunSummary> {
79
+ async function runPlan(plan: AiCliRunPlan, cwd: string): Promise<AiCliRunSummary> {
70
80
  const provider = createAiProvider({
71
81
  provider: plan.config.ai.provider,
72
82
  model: plan.config.ai.model,
@@ -92,30 +102,33 @@ async function runPlan(plan: AiCliRunPlan): Promise<AiCliRunSummary> {
92
102
  }
93
103
 
94
104
  const payload = aggregateFilesPayload(files);
95
- const outputs: string[] = [];
105
+ const pagesByLang: Partial<Record<"pt" | "en" | "es", AiDocPage[]>> = {};
96
106
 
97
107
  for (const language of plan.config.ai.languages) {
98
108
  const prompt = buildLanguagePrompt(plan.config.ai.contextPrompt, language);
99
109
  const markdown = await aiService.runGeneration(payload, prompt);
100
- const outputFile = path.posix.join(
101
- plan.config.ai.outputDir.replace(/\\/g, "/"),
102
- `${plan.config.ai.filePrefix}-${plan.config.ai.provider}-${language}.md`,
103
- );
104
- await fileSystem.writeDocumentationOutput(markdown, outputFile);
105
- outputs.push(outputFile);
110
+ pagesByLang[language] = parseAiPages(markdown);
106
111
  }
107
112
 
113
+ // Wire the AI pages into the latest documentation version (gitpagedocs pattern).
114
+ const versionId = DOC_VERSIONS[DOC_VERSIONS.length - 1] as string;
115
+ const result = await writeVersionDocs({ cwd, versionId, pagesByLang });
116
+
108
117
  return {
109
118
  scannedDirectories: scanned,
110
119
  skippedDirectories: skipped,
111
120
  scannedFilesCount: files.length,
112
- outputs,
121
+ outputs: result.files,
122
+ pages: result.slugs,
113
123
  };
114
124
  }
115
125
 
116
126
  export async function runAiCliCommand(options: {
117
127
  cwd: string;
118
128
  onInfo?: (message: string) => void;
129
+ /** Provided by the composition root to scaffold the base gitpagedocs/ tree
130
+ * (so the version config.json exists before AI pages are wired into it). */
131
+ onScaffold?: () => Promise<void>;
119
132
  }): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> {
120
133
  const logInfo = options.onInfo ?? (() => undefined);
121
134
  const configRepo = new AiConfigFileRepository(options.cwd);
@@ -128,7 +141,14 @@ export async function runAiCliCommand(options: {
128
141
  logInfo(`[gitpagedocs:ai] Configuration saved to ${configRepo.getConfigPath()}`);
129
142
  }
130
143
 
131
- const summary = await runPlan(plan);
144
+ // Build the base gitpagedocs structure BEFORE generation so the version
145
+ // config.json exists and the AI pages can be wired into it.
146
+ if (plan.runConfigScaffold && options.onScaffold) {
147
+ logInfo("[gitpagedocs] Generating base gitpagedocs structure...");
148
+ await options.onScaffold();
149
+ }
150
+
151
+ const summary = await runPlan(plan, options.cwd);
132
152
  return {
133
153
  summary,
134
154
  runConfigScaffold: plan.runConfigScaffold,
package/ai/config.ts CHANGED
@@ -19,4 +19,32 @@ export const AI_MODEL_DEFAULTS: Readonly<Record<AiProviderId, string>> = {
19
19
  export const DEFAULT_AI_DOC_PROMPT =
20
20
  'You are a senior tech writer. Describe the provided code.';
21
21
 
22
+ /**
23
+ * System prompt that teaches the model what gitpagedocs is and forces it to
24
+ * return documentation in the gitpagedocs pattern: a sequence of pages, each
25
+ * introduced by a machine-parseable delimiter line so the CLI can split the
26
+ * response into individual versioned markdown pages and wire them into the
27
+ * viewer's config.json (routes-md + menus-header-md).
28
+ */
29
+ export const GITPAGEDOCS_DOC_SYSTEM_PROMPT = [
30
+ 'You are the documentation engine for "gitpagedocs", a generator of multilingual,',
31
+ 'versioned static documentation sites. gitpagedocs renders markdown pages from',
32
+ 'gitpagedocs/docs/versions/<version>/<lang>/<page>.md and wires them into a viewer',
33
+ 'through a config.json contract (routes + header menus). Your job is to analyze the',
34
+ 'provided source code and produce clean, professional documentation that fits this',
35
+ 'pattern so it can be dropped straight into a gitpagedocs site.',
36
+ '',
37
+ 'STRICT OUTPUT FORMAT — return ONLY a sequence of pages. Begin every page with a',
38
+ 'delimiter line on its own, exactly:',
39
+ '=== PAGE: <slug> | <Title> ===',
40
+ 'followed by the page body in GitHub-flavored markdown. Rules:',
41
+ '- Produce 4 to 8 cohesive pages. Suggested slugs: overview, getting-started,',
42
+ ' architecture, configuration, usage, deployment (adapt to the project).',
43
+ '- <slug> MUST be lowercase-kebab-case and in ENGLISH, and identical across every',
44
+ ' language run, so the languages line up (only the Title and body are translated).',
45
+ '- Do NOT wrap the whole answer in a code fence. Do NOT add any preamble or epilogue',
46
+ ' outside the page blocks. The first line of your reply MUST be a "=== PAGE:" line.',
47
+ '- Base everything strictly on the analyzed files; do not invent features.',
48
+ ].join('\n');
49
+
22
50
  export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434';
@@ -28,4 +28,6 @@ export interface AiCliRunSummary {
28
28
  skippedDirectories: string[];
29
29
  scannedFilesCount: number;
30
30
  outputs: string[];
31
+ /** Slugs of the gitpagedocs pages wired into the version config (if any). */
32
+ pages?: string[];
31
33
  }
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ // @ts-expect-error .mjs runtime module is type-less in this package.
4
+ import { buildMdRoute } from "../../builders/route-builders.mjs";
5
+ import type { AiDocPage } from "../application/docs-pattern";
6
+
7
+ const CANONICAL_LANGS = ["pt", "en", "es"] as const;
8
+ type Lang = (typeof CANONICAL_LANGS)[number];
9
+
10
+ /** AI routes/menus use a high, dedicated id range so they never clash with the
11
+ * deterministic base routes (ids 1..N) and can be identified for idempotency. */
12
+ const AI_ROUTE_ID_BASE = 1000;
13
+
14
+ export interface VersionDocsWriteInput {
15
+ cwd: string;
16
+ versionId: string;
17
+ /** Pages generated per language (only the languages the user selected). */
18
+ pagesByLang: Partial<Record<Lang, AiDocPage[]>>;
19
+ }
20
+
21
+ export interface VersionDocsWriteResult {
22
+ configPath: string;
23
+ slugs: string[];
24
+ files: string[];
25
+ }
26
+
27
+ interface LangContent {
28
+ title: string;
29
+ body: string;
30
+ }
31
+
32
+ function indexBySlug(pages: AiDocPage[] | undefined): Map<string, AiDocPage> {
33
+ const map = new Map<string, AiDocPage>();
34
+ for (const page of pages ?? []) {
35
+ if (!map.has(page.slug)) map.set(page.slug, page);
36
+ }
37
+ return map;
38
+ }
39
+
40
+ /**
41
+ * Write AI-generated pages into the gitpagedocs versioned structure and wire
42
+ * them into the version config.json (routes-md + menus-header-md). Pages are
43
+ * written for ALL three canonical languages (missing languages reuse an
44
+ * available translation) so every route path stays valid. Idempotent: any prior
45
+ * `aiGenerated` routes/menus are replaced, never duplicated. The base config and
46
+ * its deterministic routes are preserved untouched.
47
+ */
48
+ export async function writeVersionDocs(input: VersionDocsWriteInput): Promise<VersionDocsWriteResult> {
49
+ const { cwd, versionId, pagesByLang } = input;
50
+ const base = `gitpagedocs/docs/versions/${versionId}`;
51
+
52
+ const perLang: Record<Lang, Map<string, AiDocPage>> = {
53
+ pt: indexBySlug(pagesByLang.pt),
54
+ en: indexBySlug(pagesByLang.en),
55
+ es: indexBySlug(pagesByLang.es),
56
+ };
57
+
58
+ // Ordered unique slug list (prefer en order, then pt, then es).
59
+ const order: string[] = [];
60
+ const seen = new Set<string>();
61
+ for (const lang of ["en", "pt", "es"] as Lang[]) {
62
+ for (const page of pagesByLang[lang] ?? []) {
63
+ if (!seen.has(page.slug)) {
64
+ seen.add(page.slug);
65
+ order.push(page.slug);
66
+ }
67
+ }
68
+ }
69
+ if (order.length === 0) {
70
+ throw new Error("No AI pages to write.");
71
+ }
72
+
73
+ const resolve = (slug: string): Record<Lang, LangContent> => {
74
+ const fallback = CANONICAL_LANGS.map((lang) => perLang[lang].get(slug)).find(Boolean);
75
+ const out = {} as Record<Lang, LangContent>;
76
+ for (const lang of CANONICAL_LANGS) {
77
+ const page = perLang[lang].get(slug) ?? fallback!;
78
+ out[lang] = { title: page.title, body: page.body };
79
+ }
80
+ return out;
81
+ };
82
+
83
+ // 1) Write the markdown files (all 3 canonical languages).
84
+ const writtenFiles: string[] = [];
85
+ for (const slug of order) {
86
+ const content = resolve(slug);
87
+ for (const lang of CANONICAL_LANGS) {
88
+ const rel = `${base}/${lang}/${slug}.md`;
89
+ const abs = path.resolve(cwd, rel);
90
+ await fs.mkdir(path.dirname(abs), { recursive: true });
91
+ await fs.writeFile(abs, `${content[lang].body}\n`, "utf-8");
92
+ writtenFiles.push(rel);
93
+ }
94
+ }
95
+
96
+ // 2) Patch the version config.json (raw JSON round-trip, preserve all keys).
97
+ const configRel = `${base}/config.json`;
98
+ const configAbs = path.resolve(cwd, configRel);
99
+ let raw: string;
100
+ try {
101
+ raw = await fs.readFile(configAbs, "utf-8");
102
+ } catch {
103
+ throw new Error(
104
+ `Version config not found at ${configRel}. Run the base scaffold (\`gitpagedocs\`) first.`,
105
+ );
106
+ }
107
+ const config = JSON.parse(raw) as Record<string, unknown>;
108
+
109
+ const routesMd = Array.isArray(config["routes-md"]) ? (config["routes-md"] as Array<Record<string, unknown>>) : [];
110
+ const menusMd = Array.isArray(config["menus-header-md"]) ? (config["menus-header-md"] as Array<Record<string, unknown>>) : [];
111
+ const keptRoutes = routesMd.filter((route) => route?.aiGenerated !== true);
112
+ const keptMenus = menusMd.filter((menu) => menu?.aiGenerated !== true);
113
+
114
+ const newRoutes: Array<Record<string, unknown>> = [];
115
+ const newMenus: Array<Record<string, unknown>> = [];
116
+ order.forEach((slug, index) => {
117
+ const content = resolve(slug);
118
+ const id = AI_ROUTE_ID_BASE + index + 1;
119
+ const pathByLang = {
120
+ pt: `${base}/pt/${slug}.md`,
121
+ en: `${base}/en/${slug}.md`,
122
+ es: `${base}/es/${slug}.md`,
123
+ };
124
+ const titles = { pt: content.pt.title, en: content.en.title, es: content.es.title };
125
+ const descriptions = { pt: "", en: "", es: "" };
126
+ const route = buildMdRoute(versionId, id, pathByLang, titles, descriptions, {}) as Record<string, unknown>;
127
+ route.aiGenerated = true;
128
+ newRoutes.push(route);
129
+ newMenus.push({
130
+ id,
131
+ aiGenerated: true,
132
+ pt: { title: content.pt.title, "path-click": pathByLang.pt },
133
+ en: { title: content.en.title, "path-click": pathByLang.en },
134
+ es: { title: content.es.title, "path-click": pathByLang.es },
135
+ });
136
+ });
137
+
138
+ config["routes-md"] = [...keptRoutes, ...newRoutes];
139
+ config["menus-header-md"] = [...keptMenus, ...newMenus];
140
+ await fs.writeFile(configAbs, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
141
+
142
+ return { configPath: configRel, slugs: order, files: writtenFiles };
143
+ }
@@ -141,7 +141,7 @@ export async function promptUseExistingConfig(existingConfig: AiCliConfig): Prom
141
141
  if (!useExistingConfig) return null;
142
142
 
143
143
  const runConfigScaffold = await askConfirm(
144
- 'After generating docs with AI, run the default gitpagedocs scaffolding?',
144
+ 'Generate the base gitpagedocs structure first, so the AI pages are wired into the docs viewer?',
145
145
  true,
146
146
  );
147
147
 
@@ -220,7 +220,7 @@ export async function runAiInteractivePrompt(existingConfig?: AiCliConfig | null
220
220
  );
221
221
 
222
222
  const runConfigScaffold = await askConfirm(
223
- 'After generating docs with AI, run the default gitpagedocs scaffolding?',
223
+ 'Generate the base gitpagedocs structure first, so the AI pages are wired into the docs viewer?',
224
224
  true,
225
225
  );
226
226
 
@@ -44,6 +44,7 @@ export function buildRootConfig(options = {}) {
44
44
  repositorySearchHome,
45
45
  rendering: renderingUrl,
46
46
  AiChatEnabled: true,
47
+ docsAccess: { enabled: false, publicKey: "" },
47
48
  langmenu: defaultLangMenu,
48
49
  },
49
50
  VersionControl: {
@@ -114,6 +114,15 @@ export function getDefaultSiteConfig(DOCS, projectLink) {
114
114
  IconNavMenuBlockInactiveReactIconesTagSize: "25px",
115
115
  IconNavMenuBlockInactiveImgWidth: 20,
116
116
  IconNavMenuBlockInactiveImgHeight: 20,
117
+ IconDocsLockLightImg: "",
118
+ IconDocsLockDarkImg: "",
119
+ IconDocsLockReactIcones: true,
120
+ IconDocsLockReactIconesTag: "FiLock",
121
+ IconDocsLockReactIconesTagColorDark: "White",
122
+ IconDocsLockReactIconesTagColorLight: "black",
123
+ IconDocsLockReactIconesTagSize: "22px",
124
+ IconDocsLockImgWidth: 20,
125
+ IconDocsLockImgHeight: 20,
117
126
  RouteguideBrandPositionDefault: "center",
118
127
  RouteguideBrandContainerTopDefault: false,
119
128
  audioPlayerEnabled: true,
@@ -38,6 +38,67 @@ export const defaultLangMenu = {
38
38
  audioPopoverStatusPaused: "Pausado",
39
39
  audioPopoverStatusLoopOn: "Loop ativado",
40
40
  audioPopoverStatusLoopOff: "Loop desativado",
41
+ aiChatTitle: "Assistente de IA",
42
+ aiChatPlaceholder: "Pergunte sobre a documentação...",
43
+ aiChatConfigTitle: "Configurar IA",
44
+ aiChatConfigDesc: "Insira sua chave de API para ativar o chat.",
45
+ aiChatClearDataBtn: "Limpar dados locais",
46
+ aiChatSendBtn: "Enviar",
47
+ aiChatSaveStartBtn: "Salvar e iniciar conversa",
48
+ aiChatAttachAriaLabel: "Anexar arquivo de áudio ou imagem",
49
+ aiChatAttachedFilesLabel: "arquivo(s) anexado(s)",
50
+ aiChatRemoveAttachmentBtn: "Remover",
51
+ aiChatCancelResponseLabel: "Cancelar resposta",
52
+ aiChatOpenBtnAriaLabel: "Abrir chat de IA",
53
+ aiChatCloseBtnAriaLabel: "Fechar chat",
54
+ 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}",
55
+ 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?",
56
+ aiChatDisclaimer: "A IA pode cometer erros. Sempre verifique as informações.",
57
+ aiChatPasswordCreateDesc: "Crie uma senha local para criptografar suas chaves de API.",
58
+ aiChatPasswordUnlockDesc: "Digite sua senha local para desbloquear.",
59
+ aiChatPasswordPlaceholder: "Senha local",
60
+ aiChatCreatePasswordBtn: "Criar senha",
61
+ aiChatUnlockBtn: "Desbloquear",
62
+ aiChatWrongPassword: "Senha incorreta.",
63
+ aiChatLockBtn: "Bloquear",
64
+ aiChatResetBtn: "Esqueceu a senha?",
65
+ aiChatResetPopupTitle: "Redefinir senha?",
66
+ aiChatResetPopupDesc: "Isso apaga todas as chaves de API salvas e a senha local. Você criará uma nova senha na próxima vez.",
67
+ aiChatResetConfirmBtn: "Sim, redefinir",
68
+ aiChatResetCancelBtn: "Cancelar",
69
+ aiChatProviderLabel: "Provedor:",
70
+ aiChatApiKeyLabel: "Chave de API (deixe em branco para IA local):",
71
+ aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)",
72
+ aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)",
73
+ aiChatProviderGemini: "Google Gemini (1.5 Flash)",
74
+ aiChatProviderOllama: "Ollama Network (Local LLMs)",
75
+ aiChatEmptyPageContent: "Nenhuma página ativa compatível.",
76
+ aiChatNoPageId: "Sem página",
77
+ aiChatOllamaUrlLabel: "URL da API Ollama (deixe em branco para local)",
78
+ aiChatClearChatPopupTitle: "Limpar conversa?",
79
+ aiChatClearChatPopupDesc: "Isso apagará todo o histórico atual do chat. Esta ação não pode ser desfeita.",
80
+ aiChatClearChatConfirmBtn: "Sim, apagar",
81
+ aiChatClearChatCancelBtn: "Cancelar",
82
+ aiChatClearDataPopupTitle: "Apagar todos os dados?",
83
+ aiChatClearDataPopupDesc: "Isso apagará todos os chats e as chaves de API locais. O chat de IA será fechado.",
84
+ aiChatClearDataConfirmBtn: "Sim, apagar tudo",
85
+ aiChatClearDataCancelBtn: "Cancelar",
86
+ aiChatUserLabel: "Você",
87
+ aiChatApiError: "Ocorreu um erro de conexão com a API.",
88
+ aiChatError401: "Chave de acesso inválida ou provedor não autorizado.",
89
+ aiChatError429: "Limite de requisições excedido. Tente novamente mais tarde.",
90
+ aiChatError500: "Ocorreu um erro interno no servidor do modelo de IA.",
91
+ aiChatErrorGeneric: "Ocorreu um erro inesperado ao se comunicar com a IA.",
92
+ docsAccessGateTitle: "Documentação protegida",
93
+ docsAccessGateDescription: "Digite a senha ou a chave privada para acessar a documentação.",
94
+ docsAccessInputPlaceholder: "Senha ou chave privada",
95
+ docsAccessUnlockBtn: "Desbloquear",
96
+ docsAccessWrongCredential: "Senha ou chave incorreta.",
97
+ docsAccessLockTooltip: "Bloquear documentação",
98
+ docsAccessBlockPopupTitle: "Bloquear a documentação?",
99
+ docsAccessBlockPopupDesc: "Você precisará da senha novamente na próxima visita.",
100
+ docsAccessBlockConfirmBtn: "Sim, bloquear",
101
+ docsAccessBlockCancelBtn: "Cancelar",
41
102
  },
42
103
  en: {
43
104
  pt: "Portuguese",
@@ -78,6 +139,67 @@ export const defaultLangMenu = {
78
139
  audioPopoverStatusPaused: "Paused",
79
140
  audioPopoverStatusLoopOn: "Loop on",
80
141
  audioPopoverStatusLoopOff: "Loop off",
142
+ aiChatTitle: "AI Assistant",
143
+ aiChatPlaceholder: "Ask about the documentation...",
144
+ aiChatConfigTitle: "Configure AI",
145
+ aiChatConfigDesc: "Enter your API key to enable the chat.",
146
+ aiChatClearDataBtn: "Clear local data",
147
+ aiChatSendBtn: "Send",
148
+ aiChatSaveStartBtn: "Save & Start Chatting",
149
+ aiChatAttachAriaLabel: "Attach audio or image file",
150
+ aiChatAttachedFilesLabel: "file(s) attached",
151
+ aiChatRemoveAttachmentBtn: "Remove",
152
+ aiChatCancelResponseLabel: "Cancel response",
153
+ aiChatOpenBtnAriaLabel: "Open AI Chat",
154
+ aiChatCloseBtnAriaLabel: "Close chat",
155
+ 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}",
156
+ 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?",
157
+ aiChatDisclaimer: "AI can make mistakes. Always verify the information.",
158
+ aiChatPasswordCreateDesc: "Create a local password to encrypt your API keys.",
159
+ aiChatPasswordUnlockDesc: "Enter your local password to unlock.",
160
+ aiChatPasswordPlaceholder: "Local password",
161
+ aiChatCreatePasswordBtn: "Create password",
162
+ aiChatUnlockBtn: "Unlock",
163
+ aiChatWrongPassword: "Incorrect password.",
164
+ aiChatLockBtn: "Lock",
165
+ aiChatResetBtn: "Forgot password?",
166
+ aiChatResetPopupTitle: "Reset password?",
167
+ aiChatResetPopupDesc: "This erases all saved API keys and the local password. You'll create a new password next time.",
168
+ aiChatResetConfirmBtn: "Yes, reset",
169
+ aiChatResetCancelBtn: "Cancel",
170
+ aiChatProviderLabel: "Provider:",
171
+ aiChatApiKeyLabel: "API Key (leave blank for Local AI):",
172
+ aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)",
173
+ aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)",
174
+ aiChatProviderGemini: "Google Gemini (1.5 Flash)",
175
+ aiChatProviderOllama: "Ollama Network (Local LLMs)",
176
+ aiChatEmptyPageContent: "No compatible active page.",
177
+ aiChatNoPageId: "No page",
178
+ aiChatOllamaUrlLabel: "Ollama API URL (leave blank for local)",
179
+ aiChatClearChatPopupTitle: "Clear conversation?",
180
+ aiChatClearChatPopupDesc: "This will delete all current chat history. This action cannot be undone.",
181
+ aiChatClearChatConfirmBtn: "Yes, delete",
182
+ aiChatClearChatCancelBtn: "Cancel",
183
+ aiChatClearDataPopupTitle: "Erase all data?",
184
+ aiChatClearDataPopupDesc: "This will delete all chats and local API keys. The AI chat will be closed.",
185
+ aiChatClearDataConfirmBtn: "Yes, erase everything",
186
+ aiChatClearDataCancelBtn: "Cancel",
187
+ aiChatUserLabel: "You",
188
+ aiChatApiError: "An API connection error occurred.",
189
+ aiChatError401: "Invalid access key or unauthorized provider.",
190
+ aiChatError429: "Rate limit exceeded. Please try again later.",
191
+ aiChatError500: "An internal server error occurred on the AI model.",
192
+ aiChatErrorGeneric: "An unexpected error occurred while communicating with the AI.",
193
+ docsAccessGateTitle: "Protected documentation",
194
+ docsAccessGateDescription: "Enter the password or private key to view the documentation.",
195
+ docsAccessInputPlaceholder: "Password or private key",
196
+ docsAccessUnlockBtn: "Unlock",
197
+ docsAccessWrongCredential: "Incorrect password or key.",
198
+ docsAccessLockTooltip: "Lock documentation",
199
+ docsAccessBlockPopupTitle: "Block the documentation?",
200
+ docsAccessBlockPopupDesc: "You'll need the password again on your next visit.",
201
+ docsAccessBlockConfirmBtn: "Yes, block",
202
+ docsAccessBlockCancelBtn: "Cancel",
81
203
  },
82
204
  es: {
83
205
  pt: "Portugues",
@@ -118,5 +240,66 @@ export const defaultLangMenu = {
118
240
  audioPauseLabel: "Pausar música de fondo",
119
241
  audioPlaylistTitle: "Elegir pista",
120
242
  audioPlaylistDescription: "Seleccione una pista para reproducir de la playlist.",
243
+ aiChatTitle: "Asistente de IA",
244
+ aiChatPlaceholder: "Pregunta sobre la documentación...",
245
+ aiChatConfigTitle: "Configurar IA",
246
+ aiChatConfigDesc: "Introduce tu clave de API para activar el chat.",
247
+ aiChatClearDataBtn: "Borrar datos locales",
248
+ aiChatSendBtn: "Enviar",
249
+ aiChatSaveStartBtn: "Guardar y empezar a chatear",
250
+ aiChatAttachAriaLabel: "Adjuntar archivo de audio o imagen",
251
+ aiChatAttachedFilesLabel: "archivo(s) adjunto(s)",
252
+ aiChatRemoveAttachmentBtn: "Quitar",
253
+ aiChatCancelResponseLabel: "Cancelar respuesta",
254
+ aiChatOpenBtnAriaLabel: "Abrir chat de IA",
255
+ aiChatCloseBtnAriaLabel: "Cerrar chat",
256
+ 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}",
257
+ 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?",
258
+ aiChatDisclaimer: "La IA puede cometer errores. Verifica siempre la información.",
259
+ aiChatPasswordCreateDesc: "Crea una contraseña local para cifrar tus claves de API.",
260
+ aiChatPasswordUnlockDesc: "Introduce tu contraseña local para desbloquear.",
261
+ aiChatPasswordPlaceholder: "Contraseña local",
262
+ aiChatCreatePasswordBtn: "Crear contraseña",
263
+ aiChatUnlockBtn: "Desbloquear",
264
+ aiChatWrongPassword: "Contraseña incorrecta.",
265
+ aiChatLockBtn: "Bloquear",
266
+ aiChatResetBtn: "¿Olvidaste la contraseña?",
267
+ aiChatResetPopupTitle: "¿Restablecer contraseña?",
268
+ aiChatResetPopupDesc: "Esto borra todas las claves de API guardadas y la contraseña local. Crearás una nueva contraseña la próxima vez.",
269
+ aiChatResetConfirmBtn: "Sí, restablecer",
270
+ aiChatResetCancelBtn: "Cancelar",
271
+ aiChatProviderLabel: "Proveedor:",
272
+ aiChatApiKeyLabel: "Clave de API (déjalo en blanco para IA local):",
273
+ aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)",
274
+ aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)",
275
+ aiChatProviderGemini: "Google Gemini (1.5 Flash)",
276
+ aiChatProviderOllama: "Ollama Network (Local LLMs)",
277
+ aiChatEmptyPageContent: "Ninguna página activa compatible.",
278
+ aiChatNoPageId: "Sin página",
279
+ aiChatOllamaUrlLabel: "URL de la API de Ollama (déjalo en blanco para local)",
280
+ aiChatClearChatPopupTitle: "¿Borrar conversación?",
281
+ aiChatClearChatPopupDesc: "Esto eliminará todo el historial de chat actual. Esta acción no se puede deshacer.",
282
+ aiChatClearChatConfirmBtn: "Sí, borrar",
283
+ aiChatClearChatCancelBtn: "Cancelar",
284
+ aiChatClearDataPopupTitle: "¿Borrar todos los datos?",
285
+ aiChatClearDataPopupDesc: "Esto eliminará todos los chats y las claves de API locales. El chat de IA se cerrará.",
286
+ aiChatClearDataConfirmBtn: "Sí, borrar todo",
287
+ aiChatClearDataCancelBtn: "Cancelar",
288
+ aiChatUserLabel: "Tú",
289
+ aiChatApiError: "Se produjo un error de conexión con la API.",
290
+ aiChatError401: "Clave de acceso no válida o proveedor no autorizado.",
291
+ aiChatError429: "Límite de solicitudes superado. Inténtalo de nuevo más tarde.",
292
+ aiChatError500: "Se produjo un error interno del servidor en el modelo de IA.",
293
+ aiChatErrorGeneric: "Se produjo un error inesperado al comunicarse con la IA.",
294
+ docsAccessGateTitle: "Documentación protegida",
295
+ docsAccessGateDescription: "Introduce la contraseña o la clave privada para ver la documentación.",
296
+ docsAccessInputPlaceholder: "Contraseña o clave privada",
297
+ docsAccessUnlockBtn: "Desbloquear",
298
+ docsAccessWrongCredential: "Contraseña o clave incorrecta.",
299
+ docsAccessLockTooltip: "Bloquear documentación",
300
+ docsAccessBlockPopupTitle: "¿Bloquear la documentación?",
301
+ docsAccessBlockPopupDesc: "Tendrás que introducir la contraseña de nuevo en la próxima visita.",
302
+ docsAccessBlockConfirmBtn: "Sí, bloquear",
303
+ docsAccessBlockCancelBtn: "Cancelar",
121
304
  },
122
305
  };
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs/promises";
2
+ import { defaultConfigLoader } from "@gitpagedocs/tools";
3
+
4
+ export interface DocsAccessConfig {
5
+ enabled: boolean;
6
+ publicKey: string;
7
+ }
8
+
9
+ /**
10
+ * Reads and patches the generated gitpagedocs/config.json IN PLACE, preserving
11
+ * every existing key (raw-JSON round-trip — it never reserializes through a typed
12
+ * model that could drop unknown fields). Used by interactive commands that need
13
+ * to persist a single value (e.g. the documentation password public key).
14
+ */
15
+ export class GitPageDocsConfigRepository {
16
+ constructor(private readonly cwd: string = process.cwd()) {}
17
+
18
+ /** Absolute path to the JSON config, or throws a friendly, actionable error. */
19
+ async resolvePath(): Promise<string> {
20
+ const resolved = await defaultConfigLoader.resolveConfigPath(this.cwd);
21
+ if (!resolved) {
22
+ throw new Error("gitpagedocs config.json not found. Run `gitpagedocs` first to generate it.");
23
+ }
24
+ if (!resolved.endsWith(".json")) {
25
+ throw new Error(`Expected a JSON config to patch, found "${resolved}". Use a gitpagedocs/config.json.`);
26
+ }
27
+ return resolved;
28
+ }
29
+
30
+ /**
31
+ * Set `site.docsAccess = { enabled, publicKey }`, leaving all other config keys
32
+ * untouched. Returns the path that was written.
33
+ */
34
+ async patchDocsAccess(publicKey: string, enabled = true): Promise<string> {
35
+ const configPath = await this.resolvePath();
36
+ const raw = await fs.readFile(configPath, "utf-8");
37
+ const config = JSON.parse(raw) as Record<string, unknown>;
38
+ const site =
39
+ config.site && typeof config.site === "object"
40
+ ? (config.site as Record<string, unknown>)
41
+ : {};
42
+ site.docsAccess = { enabled, publicKey } satisfies DocsAccessConfig;
43
+ config.site = site;
44
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
45
+ return configPath;
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "@gitpagedocs/cli",
3
- "version": "1.1.49",
4
- "description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
5
- "bin": {
6
- "gitpagedocs": "index.mjs"
7
- },
8
- "main": "index.mjs",
9
- "files": [
10
- "index.mjs",
11
- "start.mjs",
12
- "ai",
13
- "application",
14
- "builders",
15
- "content",
16
- "contracts",
17
- "data",
18
- "domain",
19
- "home",
20
- "infrastructure",
21
- "presentation",
22
- "runtime",
23
- "ui",
24
- "prebuilt",
25
- "README.md"
26
- ],
27
- "engines": {
28
- "node": ">=20"
29
- },
30
- "dependencies": {
31
- "@clack/prompts": "^1.5.1",
32
- "@gitpagedocs/mcp": "^1.1.49",
33
- "@gitpagedocs/tools": "^1.1.49",
34
- "tsx": "^4.21.0"
35
- },
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/Vidigal-code/git-page-docs.git",
39
- "directory": "cli"
40
- },
41
- "keywords": [
42
- "gitpagedocs",
43
- "documentation",
44
- "markdown",
45
- "spa",
46
- "cli",
47
- "github-pages",
48
- "create-gitpagedocs"
49
- ],
50
- "author": "Vidigal-code",
51
- "license": "ISC",
52
- "bugs": {
53
- "url": "https://github.com/Vidigal-code/git-page-docs/issues"
54
- },
55
- "homepage": "https://vidigal-code.github.io/git-page-docs/",
56
- "publishConfig": {
57
- "registry": "https://registry.npmjs.org/",
58
- "access": "public"
59
- }
60
- }
1
+ {
2
+ "name": "@gitpagedocs/cli",
3
+ "version": "1.1.52",
4
+ "description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
5
+ "bin": {
6
+ "gitpagedocs": "index.mjs"
7
+ },
8
+ "main": "index.mjs",
9
+ "files": [
10
+ "index.mjs",
11
+ "start.mjs",
12
+ "ai",
13
+ "application",
14
+ "builders",
15
+ "content",
16
+ "contracts",
17
+ "data",
18
+ "domain",
19
+ "home",
20
+ "infrastructure",
21
+ "presentation",
22
+ "runtime",
23
+ "ui",
24
+ "prebuilt",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.5.1",
32
+ "@gitpagedocs/mcp": "^1.1.52",
33
+ "@gitpagedocs/tools": "^1.1.52",
34
+ "tsx": "^4.21.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Vidigal-code/git-page-docs.git",
39
+ "directory": "cli"
40
+ },
41
+ "keywords": [
42
+ "gitpagedocs",
43
+ "documentation",
44
+ "markdown",
45
+ "spa",
46
+ "cli",
47
+ "github-pages",
48
+ "create-gitpagedocs"
49
+ ],
50
+ "author": "Vidigal-code",
51
+ "license": "ISC",
52
+ "bugs": {
53
+ "url": "https://github.com/Vidigal-code/git-page-docs/issues"
54
+ },
55
+ "homepage": "https://vidigal-code.github.io/git-page-docs/",
56
+ "publishConfig": {
57
+ "registry": "https://registry.npmjs.org/",
58
+ "access": "public"
59
+ }
60
+ }
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import type { CommandContext } from "./run-command";
4
4
  import { askConfirm } from "../ui/clack";
5
+ import { askOwnerRepo, interactivePromptsAvailable } from "../ui/prompts";
5
6
  // @ts-expect-error .mjs runtime module
6
7
  import { detectRepoFromGit, getCurrentGitBranch, tryConfigurePagesToGitHubActions } from "../../runtime/git-ops.mjs";
7
8
 
@@ -21,11 +22,14 @@ function readFlag(args: string[], name: string): string {
21
22
  * overwrite). Does NOT generate docs or push (that is `pages deploy`).
22
23
  */
23
24
  export async function runPagesActions(ctx: CommandContext): Promise<void> {
24
- const repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null;
25
+ let repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null;
25
26
  if (!repo) {
26
- // eslint-disable-next-line no-console
27
- 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");
28
- return;
27
+ if (!interactivePromptsAvailable()) {
28
+ // eslint-disable-next-line no-console
29
+ 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");
30
+ return;
31
+ }
32
+ repo = await askOwnerRepo(null);
29
33
  }
30
34
  const branch = getCurrentGitBranch(ctx.cwd) as string;
31
35
  // eslint-disable-next-line no-console
@@ -59,9 +63,14 @@ export async function runPagesDeploy(ctx: CommandContext): Promise<void> {
59
63
  }
60
64
  }
61
65
  if (!owner || !repo) {
62
- // eslint-disable-next-line no-console
63
- console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n");
64
- return;
66
+ if (!interactivePromptsAvailable()) {
67
+ // eslint-disable-next-line no-console
68
+ console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n");
69
+ return;
70
+ }
71
+ const answered = await askOwnerRepo(null, { owner, repo });
72
+ owner = answered.owner;
73
+ repo = answered.repo;
65
74
  }
66
75
 
67
76
  // eslint-disable-next-line no-console
@@ -76,7 +85,8 @@ export async function runPagesDeploy(ctx: CommandContext): Promise<void> {
76
85
  return;
77
86
  }
78
87
 
79
- const bin = path.join(ctx.pkgRoot, "cli", "index.mjs");
88
+ // pkgRoot is the cli package root (cli/), where the bin lives as index.mjs.
89
+ const bin = path.join(ctx.pkgRoot, "index.mjs");
80
90
  const pushArgs = ["--push", "--owner", owner, "--repo", repo, ...(docsPath ? ["--path", docsPath] : [])];
81
91
  const result = spawnSync(process.execPath, [bin, ...pushArgs], { stdio: "inherit", cwd: ctx.cwd, env: process.env });
82
92
 
@@ -0,0 +1,62 @@
1
+ import { NodeCryptoService, deriveDocAccessKeys } from "@gitpagedocs/tools";
2
+ import type { CommandContext } from "./run-command";
3
+ import { askPassword, note, outro } from "../ui/clack";
4
+ import { interactivePromptsAvailable } from "../ui/prompts";
5
+ import { GitPageDocsConfigRepository } from "../../infrastructure/gitpagedocs-config-file";
6
+
7
+ const MIN_PASSWORD_LENGTH = 4;
8
+
9
+ /**
10
+ * `gitpagedocs password` — interactively set a documentation access password.
11
+ *
12
+ * Type + confirm a password, derive the double-hash key pair, write the PUBLIC
13
+ * key into gitpagedocs/config.json (which makes the frontend gate the docs), and
14
+ * print the PRIVATE key for the user to copy. The password itself is never stored.
15
+ */
16
+ export async function runPassword(ctx: CommandContext): Promise<void> {
17
+ if (!interactivePromptsAvailable()) {
18
+ // eslint-disable-next-line no-console
19
+ console.log("\n `gitpagedocs password` is interactive — run it in a terminal (TTY).\n");
20
+ return;
21
+ }
22
+
23
+ const password = await askPassword({
24
+ message: "Documentation password:",
25
+ validate: (v) =>
26
+ v && v.trim().length >= MIN_PASSWORD_LENGTH
27
+ ? undefined
28
+ : `Use at least ${MIN_PASSWORD_LENGTH} characters.`,
29
+ });
30
+ const confirmation = await askPassword({
31
+ message: "Confirm password:",
32
+ validate: (v) => (v ? undefined : "Required."),
33
+ });
34
+
35
+ if (password !== confirmation) {
36
+ note("Passwords do not match. Nothing was changed.", "Aborted");
37
+ return;
38
+ }
39
+
40
+ const { privateKey, publicKey } = await deriveDocAccessKeys(password, new NodeCryptoService());
41
+
42
+ let configPath: string;
43
+ try {
44
+ configPath = await new GitPageDocsConfigRepository(ctx.cwd).patchDocsAccess(publicKey);
45
+ } catch (error) {
46
+ note(error instanceof Error ? error.message : String(error), "Could not save");
47
+ return;
48
+ }
49
+
50
+ note(
51
+ `Public key saved to ${configPath}\nThe documentation is now password-protected.`,
52
+ "Saved",
53
+ );
54
+ // eslint-disable-next-line no-console
55
+ console.log(
56
+ "\n PRIVATE KEY (copy & keep it safe — it also unlocks the docs):\n\n" +
57
+ ` ${privateKey}\n\n` +
58
+ " Readers can unlock with the password OR this private key.\n" +
59
+ " To remove protection, set site.docsAccess.enabled to false in config.json.\n",
60
+ );
61
+ outro("Done.");
62
+ }
@@ -21,6 +21,7 @@ import { runConfig } from "./config-info";
21
21
  import { runMcp } from "./mcp";
22
22
  import { runDocs } from "./docs";
23
23
  import { runPagesActions, runPagesDeploy } from "./pages";
24
+ import { runPassword } from "./password";
24
25
 
25
26
  const REGISTRY: Record<string, CommandHandler> = {
26
27
  version: runVersion,
@@ -34,6 +35,7 @@ const REGISTRY: Record<string, CommandHandler> = {
34
35
  config: runConfig,
35
36
  mcp: runMcp,
36
37
  docs: runDocs,
38
+ password: runPassword,
37
39
  };
38
40
 
39
41
  /** Returns true if a new command handled the invocation (skip legacy flow). */
@@ -78,11 +78,19 @@ async function main(): Promise<void> {
78
78
  },
79
79
  runAi: async (context) => {
80
80
  console.log("[gitpagedocs] Iniciando Módulo de IA na CLI...");
81
+ const safePrebuiltDir =
82
+ typeof context.prebuiltDir === "string"
83
+ ? context.prebuiltDir
84
+ : path.join(String(context.pkgRoot ?? process.cwd()), "prebuilt");
81
85
  // @ts-ignore
82
86
  const { runAiCliCommand } = await import("../ai/application/run-ai-cli-command.ts");
83
87
  const result = await runAiCliCommand({
84
88
  cwd: process.cwd(),
85
89
  onInfo: (message: string) => console.log(message),
90
+ // Scaffold the base gitpagedocs/ structure before AI pages are wired in.
91
+ onScaffold: async () => {
92
+ await executeConfigOnly({ ...context, prebuiltDir: safePrebuiltDir }, configOnlyRuntime);
93
+ },
86
94
  });
87
95
 
88
96
  if (result.summary.scannedFilesCount === 0) {
@@ -90,13 +98,13 @@ async function main(): Promise<void> {
90
98
  return;
91
99
  }
92
100
 
93
- if (result.runConfigScaffold) {
94
- console.log("\n[gitpagedocs] Scaffolding GitPageDocs ecosystem automatically...");
95
- const safePrebuiltDir = typeof context.prebuiltDir === "string" ? context.prebuiltDir : path.join(String(context.pkgRoot ?? process.cwd()), "prebuilt");
96
- await executeConfigOnly({ ...context, prebuiltDir: safePrebuiltDir }, configOnlyRuntime);
97
- }
98
-
99
- console.log(`\n🎉 Processo completo! Arquivos gerados: ${result.summary.outputs.join(", ")}`);
101
+ const pages = result.summary.pages ?? [];
102
+ console.log(
103
+ `\n🎉 Processo completo! Páginas no padrão gitpagedocs: ${pages.join(", ") || "(nenhuma)"}`,
104
+ );
105
+ console.log(
106
+ `[gitpagedocs:ai] ${result.summary.outputs.length} arquivos markdown gerados e conectados ao config.json da versão mais recente.`,
107
+ );
100
108
  },
101
109
  });
102
110
  printCredits();
@@ -1,7 +1,16 @@
1
1
  import type { CliOptions } from "../../domain/models/cli-options";
2
2
  import { parseCliOptions } from "./parser";
3
- import { shouldRunInteractive, promptConfigOnlyOptions, promptHomeOptions } from "../ui/prompts";
3
+ import {
4
+ shouldRunInteractive,
5
+ promptConfigOnlyOptions,
6
+ promptHomeOptions,
7
+ promptDeployOptions,
8
+ ensureGitRepoInteractive,
9
+ interactivePromptsAvailable,
10
+ } from "../ui/prompts";
4
11
  import { DEFAULTS } from "./schema";
12
+ // @ts-expect-error .mjs runtime module is type-less in this package.
13
+ import { detectRepoFromGit } from "../../runtime/git-ops.mjs";
5
14
 
6
15
  export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Promise<CliOptions> {
7
16
  const parsed = parseCliOptions(argv, env);
@@ -11,6 +20,20 @@ export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Pr
11
20
  parsed.basePath = parsed.basePath ?? parsed.docsPath ?? DEFAULTS.home.basePath;
12
21
  }
13
22
 
23
+ // `deploy` / `--push` need owner + repo AND a git repo. If anything is
24
+ // missing, prompt interactively (owner/repo pre-filled from the git origin
25
+ // remote; offer to `git init`) instead of throwing. In CI/non-TTY we fall
26
+ // through so the explicit guards in the push flow still fire clearly.
27
+ if (parsed.shouldPush && interactivePromptsAvailable()) {
28
+ let resolved = parsed;
29
+ if (!parsed.githubOwner || !parsed.githubRepo) {
30
+ const detected = detectRepoFromGit(process.cwd()) as { owner: string; repo: string } | null;
31
+ resolved = await promptDeployOptions(parsed, detected);
32
+ }
33
+ await ensureGitRepoInteractive(process.cwd());
34
+ return resolved;
35
+ }
36
+
14
37
  const runHomeInteractive = parsed.isInteractive || (shouldRunInteractive(argv) && parsed.mode === "home");
15
38
  if (runHomeInteractive && parsed.mode === "home") {
16
39
  return promptHomeOptions(parsed);
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import {
7
7
  text,
8
+ password,
8
9
  confirm,
9
10
  select,
10
11
  multiselect,
@@ -40,6 +41,22 @@ export async function askText(options: AskTextOptions): Promise<string> {
40
41
  return exitIfCancelled(result);
41
42
  }
42
43
 
44
+ export interface AskPasswordOptions {
45
+ message: string;
46
+ validate?: (value: string) => string | undefined;
47
+ }
48
+
49
+ /** Masked password prompt (input hidden as the user types). */
50
+ export async function askPassword(options: AskPasswordOptions): Promise<string> {
51
+ const result = await password({
52
+ message: options.message,
53
+ validate: options.validate
54
+ ? (value) => options.validate?.(value ?? "")
55
+ : undefined,
56
+ });
57
+ return exitIfCancelled(result);
58
+ }
59
+
43
60
  export async function askConfirm(message: string, initialValue = false): Promise<boolean> {
44
61
  const result = await confirm({ message, initialValue });
45
62
  return exitIfCancelled(result);
@@ -1,6 +1,9 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
1
4
  import type { CliOptions } from "../../domain/models/cli-options";
2
5
  import { DEFAULTS } from "../options/schema";
3
- import { askText, askConfirm } from "./clack";
6
+ import { askText, askConfirm, note } from "./clack";
4
7
 
5
8
  function isCiOrNonTty(): boolean {
6
9
  if (process.env.CI === "true") return true;
@@ -9,6 +12,11 @@ function isCiOrNonTty(): boolean {
9
12
  return false;
10
13
  }
11
14
 
15
+ /** True when clack prompts can run (interactive TTY, not CI). */
16
+ export function interactivePromptsAvailable(): boolean {
17
+ return !isCiOrNonTty();
18
+ }
19
+
12
20
  export function shouldRunInteractive(argv: string[]): boolean {
13
21
  if (isCiOrNonTty()) return false;
14
22
  const args = argv.slice(2);
@@ -41,6 +49,72 @@ export async function promptHomeOptions(parsed: CliOptions): Promise<CliOptions>
41
49
  };
42
50
  }
43
51
 
52
+ /**
53
+ * Deploy/--push needs a GitHub owner + repo. When they are missing, prompt for
54
+ * them (pre-filled from the git `origin` remote when available) instead of
55
+ * crashing with "`--push` requires owner and repo".
56
+ */
57
+ export async function askOwnerRepo(
58
+ detected: { owner: string; repo: string } | null,
59
+ defaults?: { owner?: string; repo?: string },
60
+ ): Promise<{ owner: string; repo: string }> {
61
+ const owner = await askText({
62
+ message: "GitHub owner (user or organization):",
63
+ defaultValue: defaults?.owner || detected?.owner || "",
64
+ validate: (v) => (v && v.trim() ? undefined : "Owner is required."),
65
+ });
66
+ const repo = await askText({
67
+ message: "GitHub repository name:",
68
+ defaultValue: defaults?.repo || detected?.repo || "",
69
+ validate: (v) => (v && v.trim() ? undefined : "Repository is required."),
70
+ });
71
+ return { owner: owner.trim(), repo: repo.trim() };
72
+ }
73
+
74
+ export async function promptDeployOptions(
75
+ parsed: CliOptions,
76
+ detected: { owner: string; repo: string } | null,
77
+ ): Promise<CliOptions> {
78
+ note(
79
+ "Deploy publishes gitpagedocs to GitHub Pages and needs the target\nrepository (owner/repo). Press Enter to accept a detected value.",
80
+ "GitHub Pages deploy",
81
+ );
82
+ const { owner, repo } = await askOwnerRepo(detected, {
83
+ owner: parsed.githubOwner,
84
+ repo: parsed.githubRepo,
85
+ });
86
+
87
+ return {
88
+ ...parsed,
89
+ githubOwner: owner,
90
+ githubRepo: repo,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Deploy/--push needs a git repository in the current folder. When there isn't
96
+ * one, offer to run `git init` instead of crashing with "not a git repository".
97
+ * In CI/non-TTY this is a no-op so the downstream guard still reports clearly.
98
+ */
99
+ export async function ensureGitRepoInteractive(root: string): Promise<void> {
100
+ if (existsSync(path.join(root, ".git"))) return;
101
+ if (!interactivePromptsAvailable()) return;
102
+ const init = await askConfirm(
103
+ "This folder is not a git repository yet. Initialize one here (git init)?",
104
+ true,
105
+ );
106
+ if (!init) {
107
+ note("Deploy needs a git repository. Run `git init`, then retry.", "Heads up");
108
+ return;
109
+ }
110
+ try {
111
+ execSync("git init", { cwd: root, stdio: "ignore" });
112
+ note("Initialized an empty git repository.", "git");
113
+ } catch {
114
+ note("Could not run `git init`. Initialize git manually, then retry.", "git");
115
+ }
116
+ }
117
+
44
118
  export async function promptConfigOnlyOptions(parsed: CliOptions): Promise<CliOptions> {
45
119
  const useLocalLayoutConfig = await askConfirm(
46
120
  "Generate local layout templates?",