@gitpagedocs/cli 0.0.1

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.
Files changed (91) hide show
  1. package/README.md +193 -0
  2. package/ai/application/ai-command.ts +24 -0
  3. package/ai/application/ai-provider-factory.ts +22 -0
  4. package/ai/application/docs-pattern.ts +72 -0
  5. package/ai/application/resolve-chat-credentials.ts +69 -0
  6. package/ai/application/run-ai-cli-command.ts +167 -0
  7. package/ai/config.ts +50 -0
  8. package/ai/core/chat-session.ts +44 -0
  9. package/ai/core/models/ai-cli-config.ts +33 -0
  10. package/ai/core/models/parse-ai-cli-config.ts +76 -0
  11. package/ai/core/ports/illm-provider.ts +32 -0
  12. package/ai/infrastructure/ai-config-file.ts +101 -0
  13. package/ai/infrastructure/file-system-adapter.ts +85 -0
  14. package/ai/infrastructure/llm/tools-llm-provider.ts +54 -0
  15. package/ai/infrastructure/version-docs-writer.ts +143 -0
  16. package/ai/presentation/ai-prompts.ts +239 -0
  17. package/application/config-only/handler.mjs +57 -0
  18. package/application/home/handler.mjs +87 -0
  19. package/application/ports/cli-runtime-ports.ts +93 -0
  20. package/application/report/config-only-reporter.mjs +68 -0
  21. package/application/use-cases/dispatch-mode.ts +18 -0
  22. package/builders/config-orchestrator.mjs +35 -0
  23. package/builders/language-bundles-builder.mjs +24 -0
  24. package/builders/project-links.mjs +50 -0
  25. package/builders/root-config-builder.d.mts +16 -0
  26. package/builders/root-config-builder.mjs +56 -0
  27. package/builders/route-builders.mjs +136 -0
  28. package/builders/theme-template.mjs +76 -0
  29. package/builders/version-config-builder.mjs +186 -0
  30. package/content/docs-en.mjs +556 -0
  31. package/content/docs-es.mjs +469 -0
  32. package/content/docs-pt.mjs +469 -0
  33. package/content/docs.mjs +9 -0
  34. package/contracts/doc-versions.mjs +3 -0
  35. package/contracts/index.mjs +11 -0
  36. package/contracts/langs-paths.mjs +28 -0
  37. package/contracts/languages.mjs +6 -0
  38. package/contracts/layouts-paths.d.mts +17 -0
  39. package/contracts/layouts-paths.mjs +68 -0
  40. package/contracts/path-segments.d.mts +2 -0
  41. package/contracts/path-segments.mjs +15 -0
  42. package/data/default-site-config.mjs +328 -0
  43. package/data/i18n-langmenu.mjs +350 -0
  44. package/data/i18n-translations.mjs +20 -0
  45. package/data/layouts.mjs +437 -0
  46. package/data/path-mappings.mjs +26 -0
  47. package/data/route-metas.mjs +93 -0
  48. package/data/theme-colors.mjs +444 -0
  49. package/data/urls.mjs +12 -0
  50. package/data/version-constants.mjs +3 -0
  51. package/domain/models/cli-options.ts +33 -0
  52. package/domain/services/layouts-migration.d.mts +15 -0
  53. package/domain/services/layouts-migration.mjs +74 -0
  54. package/domain/services/sanitize-segment.mjs +7 -0
  55. package/home/constants.mjs +4 -0
  56. package/home/home-file-writer.mjs +19 -0
  57. package/home/templates.mjs +51 -0
  58. package/index.mjs +38 -0
  59. package/infrastructure/config-only/runtime.mjs +17 -0
  60. package/infrastructure/gitpagedocs-config-file.ts +47 -0
  61. package/infrastructure/home/runtime.mjs +37 -0
  62. package/package.json +60 -0
  63. package/presentation/commands/ai-info.ts +57 -0
  64. package/presentation/commands/chat.ts +259 -0
  65. package/presentation/commands/config-clear.ts +23 -0
  66. package/presentation/commands/config-info.ts +72 -0
  67. package/presentation/commands/diagnostics.ts +174 -0
  68. package/presentation/commands/docs.ts +42 -0
  69. package/presentation/commands/mcp.ts +20 -0
  70. package/presentation/commands/pages.ts +103 -0
  71. package/presentation/commands/password.ts +62 -0
  72. package/presentation/commands/run-command.ts +72 -0
  73. package/presentation/index.ts +125 -0
  74. package/presentation/options/parser.ts +120 -0
  75. package/presentation/options/resolver.ts +46 -0
  76. package/presentation/options/schema.ts +14 -0
  77. package/presentation/ui/banner.ts +13 -0
  78. package/presentation/ui/clack.ts +91 -0
  79. package/presentation/ui/layouts-prompts.ts +124 -0
  80. package/presentation/ui/prompts.ts +147 -0
  81. package/presentation/ui/terminal-stream.ts +97 -0
  82. package/presentation/ui/tty.ts +37 -0
  83. package/runtime/doc-path-resolver.mjs +59 -0
  84. package/runtime/exec.mjs +168 -0
  85. package/runtime/git-ops.mjs +112 -0
  86. package/runtime/layouts-migrator.d.mts +11 -0
  87. package/runtime/layouts-migrator.mjs +117 -0
  88. package/runtime/output.mjs +201 -0
  89. package/runtime/workflow.mjs +131 -0
  90. package/start.mjs +14 -0
  91. package/ui/logger.mjs +9 -0
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # @gitpagedocs/cli
2
+
3
+ The **published npm package** of the [Git Page Docs](../README.md) monorepo (the command it installs is `gitpagedocs`). It scaffolds the `gitpagedocs/` docs contract, generates docs with AI, configures GitHub Pages, and runs the MCP server.
4
+
5
+ ```bash
6
+ npm install -g @gitpagedocs/cli # global (installs the `gitpagedocs` command)
7
+ gitpagedocs # generate gitpagedocs/ config (config-only)
8
+
9
+ # or one-off, no install:
10
+ npx @gitpagedocs/cli
11
+ ```
12
+
13
+ > This package ships its TypeScript sources and runs them via `tsx`; it depends on the workspace packages `@gitpagedocs/tools` and `@gitpagedocs/mcp` (published alongside it).
14
+
15
+ ## Commands
16
+
17
+ | Command | Description |
18
+ |---|---|
19
+ | `gitpagedocs` | Generate `gitpagedocs/` config + versioned docs (config-only) |
20
+ | `gitpagedocs --layoutconfig` | Also emit local layout templates in `gitpagelayouts/` |
21
+ | `gitpagedocs --layouts-dir <dir>` | Folder for local layouts (default: `gitpagelayouts`) |
22
+ | `gitpagedocs --no-interactive` / `-y` | Never prompt; use flags and defaults |
23
+ | `gitpagedocs --push --owner <o> --repo <r> [--path <sub>]` | Generate, configure Pages, create workflow, commit + push |
24
+ | `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`: static site + .env + Dockerfile) |
25
+ | `gitpagedocs -i` / `--interactive` | Interactive prompts |
26
+ | `gitpagedocs ai` | Interactive AI documentation generator — writes pages in the gitpagedocs pattern (see below) |
27
+ | `gitpagedocs provider [id]` · `models [provider]` | List AI providers / catalog models |
28
+ | `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI in the gitpagedocs pattern |
29
+ | `gitpagedocs password` | Set a documentation access password (writes the public key to `config.json`, prints the private key) |
30
+ | `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push |
31
+ | `gitpagedocs docs` | Refresh managed regions of README/CONTRIBUTING/SECURITY |
32
+ | `gitpagedocs config clear` | Delete the stored `.gitpagedocsconfig` (wipes saved AI credentials) |
33
+ | `gitpagedocs doctor` · `version` · `update` | Diagnostics / version / registry update check |
34
+ | `gitpagedocs mcp start` | Start the MCP server over stdio |
35
+
36
+ The AI-CLI implementation lives in `cli/ai/` (relocated from the frontend so the published package is self-contained).
37
+
38
+ # CLI Architecture (Clean Architecture + SOLID)
39
+
40
+ ## Layer Overview
41
+
42
+ ```
43
+ cli/
44
+ |-- index.mjs # Node entry (bootstraps TS presentation via tsx)
45
+ |-- package.json # the published `gitpagedocs` package
46
+ |-- ai/ # AI documentation CLI (provider/model/paths → markdown)
47
+ |-- contracts/ # Stable contracts for external tooling
48
+ | `-- doc-versions.mjs
49
+ |
50
+ |-- presentation/ # Interface/composition root
51
+ | |-- index.ts
52
+ | |-- options/
53
+ | `-- ui/
54
+ |
55
+ |-- application/ # Use-cases and ports
56
+ | |-- config-only/handler.mjs
57
+ | |-- home/handler.mjs
58
+ | |-- report/config-only-reporter.mjs
59
+ | |-- use-cases/dispatch-mode.ts
60
+ | `-- ports/cli-runtime-ports.ts
61
+ |
62
+ |-- domain/ # Business rules
63
+ | `-- services/sanitize-segment.mjs
64
+ |
65
+ |-- infrastructure/ # Side effects and adapters
66
+ | |-- config-only/runtime.mjs
67
+ | `-- home/runtime.mjs
68
+ |
69
+ |-- builders/ # Pure config builders
70
+ |-- data/ # Constants and metadata
71
+ |-- content/ # Static docs content
72
+ |-- runtime/ # Low-level runtime helpers
73
+ `-- home/ # Home templates/files
74
+ ```
75
+
76
+ ## SOLID Principles
77
+
78
+ | Principle | Application |
79
+ |-----------|-------------|
80
+ | **SRP** | `application/report` only formats messages; handlers only orchestrate |
81
+ | **OCP** | New runtime implementations can be injected through ports |
82
+ | **LSP** | `dispatch-mode` works with any runner implementing `CliCommandRunner` |
83
+ | **ISP** | Split runtime contracts (`ConfigOnlyRuntimePort`, `HomeRuntimePort`) |
84
+ | **DIP** | Use-cases depend on `application/ports`, not concrete fs/git/exec |
85
+
86
+ ## Clean Architecture
87
+
88
+ - **Interfaces/Presentation**: parse CLI args, resolve mode, wire dependencies in `presentation/index.ts`
89
+ - **Application**: orchestrate flows with explicit ports, without direct fs/git/exec imports
90
+ - **Domain**: sanitize and core rules with no infrastructure dependencies
91
+ - **Infrastructure**: concrete adapters for runtime side effects
92
+ - **Contracts**: `tools/smoke` consumes `cli/contracts` instead of internal CLI modules
93
+
94
+ ## Authorized Routes Pipeline
95
+
96
+ The CLI now generates authorized-route docs and config entries in PT/EN/ES.
97
+
98
+ - Source content: `cli/content/docs.mjs` (`authorizedRoutes` key per language)
99
+ - Route metadata: `cli/data/route-metas.mjs` (`ROUTE_META_ID6`)
100
+ - Route path mapping: `cli/data/path-mappings.mjs` (`authorized-routes.md`)
101
+ - Version config composition: `cli/builders/version-config-builder.mjs`
102
+ - Doc key resolution: `cli/runtime/doc-path-resolver.mjs`
103
+
104
+ Generated artifacts include:
105
+
106
+ - `gitpagedocs/docs/versions/<version>/config.json` with `auth` and route `authorization`
107
+ - `gitpagedocs/docs/versions/<version>/{pt,en,es}/authorized-routes.md`
108
+ - `menus-header-md` entry dedicated to Authorized Routes
109
+
110
+ ## GitHub Pages Push Paths
111
+
112
+ - Default project-site publish (`--push` without `--path`) targets `/<repo>/`.
113
+ - Custom project subpath publish (`--push --path <segment>`) targets `/<repo>/<segment>/`.
114
+ - `--path` accepts a single sanitized segment (letters, numbers, `.`, `_`, `-`).
115
+
116
+ Example:
117
+
118
+ ```bash
119
+ npx @gitpagedocs/cli --owner vidigal-code --repo energy-bill-ai-parser --path git-docs --push
120
+ ```
121
+
122
+ Expected URL after deployment:
123
+
124
+ ```text
125
+ https://vidigal-code.github.io/energy-bill-ai-parser/git-docs/
126
+ ```
127
+
128
+ ## AI CLI (interactive + config file)
129
+
130
+ The CLI now includes a dedicated interactive AI mode:
131
+
132
+ ```bash
133
+ npx @gitpagedocs/cli ai
134
+ ```
135
+
136
+ ### What it does
137
+
138
+ - asks provider (`openai`, `claude`, `gemini`, `ollama`)
139
+ - asks API key or Ollama URL
140
+ - asks paths to scan (supports one or many paths, including other repositories)
141
+ - generates documentation in `pt`, `en`, `es` **in the gitpagedocs pattern**
142
+ - optionally persists config in `.gitpagedocsconfig`
143
+
144
+ ### gitpagedocs pattern output
145
+
146
+ The model is given a gitpagedocs-aware system prompt and returns documentation split into
147
+ pages (delimited by `=== PAGE: <slug> | <Title> ===`). The CLI scaffolds the base
148
+ `gitpagedocs/` structure first, then writes each page to
149
+ `gitpagedocs/docs/versions/<latest>/<lang>/<slug>.md` for every language and **wires it into
150
+ that version's `config.json`** (`routes-md` + `menus-header-md`) so the pages render in the
151
+ viewer menu. Entries are tagged `aiGenerated` and idempotent (re-running replaces them). A
152
+ later plain `gitpagedocs` rebuild drops the wiring — re-run `gitpagedocs ai` to restore.
153
+
154
+ ### `.gitpagedocsconfig` (manual mode)
155
+
156
+ The file lives in the per-user OS config directory (never inside the repository, so
157
+ the API key cannot be committed by accident) and is written with owner-only
158
+ permissions on POSIX systems:
159
+
160
+ - Windows: `%APPDATA%\gitpagedocs\.gitpagedocsconfig`
161
+ - macOS: `~/Library/Application Support/gitpagedocs/.gitpagedocsconfig`
162
+ - Linux: `$XDG_CONFIG_HOME/gitpagedocs/.gitpagedocsconfig` (or `~/.config/gitpagedocs/.gitpagedocsconfig`)
163
+
164
+ Set `GITPAGEDOCS_CONFIG_DIR` to override the directory; delete the stored file
165
+ and its credentials with `gitpagedocs config clear`; a legacy
166
+ `.gitpagedocsconfig` in the repository root is migrated automatically on the next
167
+ run. You can create/edit this file manually and then run `npx @gitpagedocs/cli ai`:
168
+
169
+ ```json
170
+ {
171
+ "version": 1,
172
+ "ai": {
173
+ "provider": "openai",
174
+ "model": "gpt-4o-mini",
175
+ "apiKey": "<YOUR_API_KEY>",
176
+ "paths": ["src", "cli", "../another-repo/src"],
177
+ "languages": ["pt", "en", "es"],
178
+ "outputDir": "gitpagedocs/docs",
179
+ "filePrefix": "ai-generated",
180
+ "contextPrompt": "Você é um redator técnico sênior..."
181
+ }
182
+ }
183
+ ```
184
+
185
+ For Ollama, use `baseUrl` instead of `apiKey`.
186
+
187
+ ### Interactive fallback
188
+
189
+ If a directory is not found, CLI offers fallback choices:
190
+
191
+ - fix paths and retry
192
+ - skip missing paths and continue
193
+ - abort safely
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file ai-command.ts
3
+ * @description Application service for running AI commands. Decoupled from concrete LLMs.
4
+ */
5
+ import { ILLMProvider } from '../core/ports/illm-provider';
6
+
7
+ export class AiCommandService {
8
+ constructor(private readonly provider: ILLMProvider) { }
9
+
10
+ /**
11
+ * Executes the AI Documentation Generation for a specific content
12
+ */
13
+ async runGeneration(content: string, contextPrompt?: string): Promise<string> {
14
+ return await this.provider.generateDocumentation(content, contextPrompt);
15
+ }
16
+
17
+ /**
18
+ * Executes a CLI chat prompt
19
+ */
20
+ async runChat(prompt: string): Promise<string> {
21
+ const messages = [{ role: 'user' as const, content: prompt }];
22
+ return await this.provider.chat(messages);
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ import type { AiProviderId } from "../config";
2
+ import type { ILLMProvider } from "../core/ports/illm-provider";
3
+ import { ToolsLlmProvider } from "../infrastructure/llm/tools-llm-provider";
4
+
5
+ /**
6
+ * Creates the CLI LLM provider. Backed by the shared @gitpagedocs/tools AI core
7
+ * (one registry-driven implementation for every provider) — no per-provider
8
+ * switch. The legacy provider id (openai/claude/gemini/ollama) is mapped to the
9
+ * catalog inside ToolsLlmProvider.
10
+ */
11
+ export function createAiProvider(input: {
12
+ provider: AiProviderId;
13
+ model: string;
14
+ apiKey?: string;
15
+ baseUrl?: string;
16
+ }): ILLMProvider {
17
+ return new ToolsLlmProvider(input.provider, {
18
+ model: input.model,
19
+ apiKey: input.apiKey,
20
+ baseUrl: input.baseUrl,
21
+ });
22
+ }
@@ -0,0 +1,72 @@
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
+ // Slug is everything up to the first `|`, title everything up to the closing
18
+ // `===`; both are trimmed by the caller. Kept free of overlapping quantifiers
19
+ // so a hostile line cannot make the match super-linear.
20
+ const PAGE_DELIMITER = /^===\s*PAGE:([^|]*)\|(.*)===\s*$/;
21
+
22
+ /** Turn an arbitrary heading into a safe lowercase-kebab slug. */
23
+ export function slugify(raw: string): string {
24
+ const slug = (raw ?? "")
25
+ .toLowerCase()
26
+ .normalize("NFKD")
27
+ .split(/[^a-z0-9]+/)
28
+ .filter(Boolean)
29
+ .join("-")
30
+ .slice(0, 60);
31
+ return slug || "page";
32
+ }
33
+
34
+ export function parseAiPages(markdown: string): AiDocPage[] {
35
+ const text = (markdown ?? "").replaceAll("\r\n", "\n").trim();
36
+ if (!text) return [];
37
+
38
+ const pages: AiDocPage[] = [];
39
+ let current: { slug: string; title: string; body: string[] } | null = null;
40
+
41
+ const flush = () => {
42
+ if (current) {
43
+ pages.push({ slug: current.slug, title: current.title, body: current.body.join("\n").trim() });
44
+ }
45
+ };
46
+
47
+ for (const line of text.split("\n")) {
48
+ const match = PAGE_DELIMITER.exec(line);
49
+ if (match) {
50
+ flush();
51
+ const rawSlug = match[1].trim();
52
+ const title = match[2].trim() || rawSlug;
53
+ current = { slug: slugify(rawSlug), title, body: [] };
54
+ } else if (current) {
55
+ current.body.push(line);
56
+ }
57
+ }
58
+ flush();
59
+
60
+ const withBody = pages.filter((page) => page.body.length > 0);
61
+ if (withBody.length === 0) {
62
+ return [{ slug: "ai-overview", title: "Overview", body: text }];
63
+ }
64
+
65
+ // De-duplicate slugs within a single response (suffix later collisions).
66
+ const seen = new Map<string, number>();
67
+ return withBody.map((page) => {
68
+ const count = seen.get(page.slug) ?? 0;
69
+ seen.set(page.slug, count + 1);
70
+ return count === 0 ? page : { ...page, slug: `${page.slug}-${count + 1}` };
71
+ });
72
+ }
@@ -0,0 +1,69 @@
1
+ import { legacyProviderToCatalogId, resolveApiKeyFromEnv } from "@gitpagedocs/tools/ai";
2
+ import type { AiProviderId } from "@gitpagedocs/tools/ports";
3
+ import { PROVIDER_CATALOG } from "@gitpagedocs/tools";
4
+ import { AiConfigFileRepository } from "../infrastructure/ai-config-file";
5
+ import type { AiCliConfig } from "../core/models/ai-cli-config";
6
+
7
+ export interface ResolvedChatCredentials {
8
+ readonly providerId: AiProviderId;
9
+ readonly model: string;
10
+ readonly apiKey?: string;
11
+ readonly baseUrl?: string;
12
+ }
13
+
14
+ export interface ResolveChatCredentialsInput {
15
+ readonly cwd: string;
16
+ /** Overrides the stored/detected provider (e.g. a `--provider` flag). */
17
+ readonly providerOverride?: string;
18
+ /** Overrides the stored/detected model (e.g. a `--model` flag). */
19
+ readonly modelOverride?: string;
20
+ /** Injectable for tests; defaults to a real config file repository. */
21
+ readonly configRepo?: Pick<AiConfigFileRepository, "read">;
22
+ /** Injectable for tests; defaults to process.env. */
23
+ readonly env?: Readonly<Record<string, string | undefined>>;
24
+ }
25
+
26
+ /**
27
+ * Resolves the provider, model and credentials for a chat turn, layering
28
+ * sources by priority: explicit overrides > stored .gitpagedocsconfig > env
29
+ * vars declared by the catalog. Returns null when no provider can be
30
+ * determined and no key/base URL is available, so the caller can guide setup.
31
+ */
32
+ export async function resolveChatCredentials(
33
+ input: ResolveChatCredentialsInput,
34
+ ): Promise<ResolvedChatCredentials | null> {
35
+ const env = input.env ?? process.env;
36
+ const configRepo = input.configRepo ?? new AiConfigFileRepository({ cwd: input.cwd });
37
+ const stored = await configRepo.read().catch(() => null);
38
+
39
+ const providerId = resolveProviderId(input.providerOverride, stored, env);
40
+ const spec = PROVIDER_CATALOG[providerId];
41
+ const model = input.modelOverride?.trim() || stored?.ai.model?.trim() || spec.defaultModel;
42
+ const baseUrl = stored?.ai.baseUrl?.trim() || undefined;
43
+ const apiKey = stored?.ai.apiKey?.trim() || resolveApiKeyFromEnv(providerId, env);
44
+
45
+ const keyless = spec.auth === "none";
46
+ if (!keyless && !apiKey) return null;
47
+
48
+ return { providerId, model, apiKey, baseUrl };
49
+ }
50
+
51
+ /** Explicit override > stored config > first provider with an env key > openai. */
52
+ function resolveProviderId(
53
+ override: string | undefined,
54
+ stored: AiCliConfig | null,
55
+ env: Readonly<Record<string, string | undefined>>,
56
+ ): AiProviderId {
57
+ if (override) return legacyProviderToCatalogId(override);
58
+ if (stored) return legacyProviderToCatalogId(stored.ai.provider);
59
+ return firstProviderFromEnv(env) ?? "openai";
60
+ }
61
+
62
+ function firstProviderFromEnv(
63
+ env: Readonly<Record<string, string | undefined>>,
64
+ ): AiProviderId | undefined {
65
+ for (const id of Object.keys(PROVIDER_CATALOG) as AiProviderId[]) {
66
+ if (resolveApiKeyFromEnv(id, env)) return id;
67
+ }
68
+ return undefined;
69
+ }
@@ -0,0 +1,167 @@
1
+ import { AiCommandService } from "./ai-command";
2
+ import { createAiProvider } from "./ai-provider-factory";
3
+ import { FileSystemAdapter, type FilePayload } from "../infrastructure/file-system-adapter";
4
+ import {
5
+ AiConfigFileRepository,
6
+ } from "../infrastructure/ai-config-file";
7
+ import type { AiCliRunPlan, AiCliRunSummary } from "../core/models/ai-cli-config";
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";
14
+
15
+ const LANGUAGE_TO_LABEL: Record<string, string> = {
16
+ pt: "Portuguese",
17
+ en: "English",
18
+ es: "Español",
19
+ };
20
+
21
+ function aggregateFilesPayload(files: FilePayload[]): string {
22
+ return files
23
+ .map((file) => `\n\n--- FILE: ${file.filePath} ---\n\n${file.content}`)
24
+ .join("");
25
+ }
26
+
27
+ function buildLanguagePrompt(basePrompt: string, language: "pt" | "en" | "es"): string {
28
+ const languageLabel = LANGUAGE_TO_LABEL[language] ?? language;
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
+ );
36
+ }
37
+
38
+ async function collectFilesWithInteractiveFallback(
39
+ fileSystem: FileSystemAdapter,
40
+ initialPaths: string[],
41
+ ): Promise<{ files: FilePayload[]; scanned: string[]; skipped: string[] }> {
42
+ let queue = [...initialPaths];
43
+ let scanned: string[] = [];
44
+ let skipped: string[] = [];
45
+ const files: FilePayload[] = [];
46
+
47
+ while (queue.length > 0) {
48
+ const { existing, missing } = fileSystem.splitExistingDirectories(queue);
49
+ scanned = [...scanned, ...existing];
50
+
51
+ for (const directory of existing) {
52
+ const directoryFiles = await fileSystem.readDirectoryRecursively(directory);
53
+ files.push(...directoryFiles);
54
+ }
55
+
56
+ if (!missing.length) break;
57
+
58
+ const fallback = await promptMissingDirectories(missing);
59
+ if (fallback.abort) {
60
+ skipped = [...skipped, ...missing];
61
+ break;
62
+ }
63
+
64
+ if (!fallback.replacementPaths.length) {
65
+ skipped = [...skipped, ...missing];
66
+ break;
67
+ }
68
+
69
+ queue = fallback.replacementPaths;
70
+ }
71
+
72
+ return {
73
+ files,
74
+ scanned,
75
+ skipped,
76
+ };
77
+ }
78
+
79
+ /** Factory seam so tests can inject a deterministic provider (DIP). */
80
+ export type AiProviderFactory = typeof createAiProvider;
81
+
82
+ export async function executeAiRunPlan(
83
+ plan: AiCliRunPlan,
84
+ cwd: string,
85
+ createProvider: AiProviderFactory = createAiProvider,
86
+ ): Promise<AiCliRunSummary> {
87
+ const provider = createProvider({
88
+ provider: plan.config.ai.provider,
89
+ model: plan.config.ai.model,
90
+ apiKey: plan.config.ai.apiKey,
91
+ baseUrl: plan.config.ai.baseUrl,
92
+ });
93
+
94
+ const aiService = new AiCommandService(provider);
95
+ const fileSystem = new FileSystemAdapter(cwd);
96
+
97
+ const { files, scanned, skipped } = await collectFilesWithInteractiveFallback(
98
+ fileSystem,
99
+ plan.config.ai.paths,
100
+ );
101
+
102
+ if (!files.length) {
103
+ return {
104
+ scannedDirectories: scanned,
105
+ skippedDirectories: skipped,
106
+ scannedFilesCount: 0,
107
+ outputs: [],
108
+ };
109
+ }
110
+
111
+ const payload = aggregateFilesPayload(files);
112
+ const pagesByLang: Partial<Record<"pt" | "en" | "es", AiDocPage[]>> = {};
113
+
114
+ for (const language of plan.config.ai.languages) {
115
+ const prompt = buildLanguagePrompt(plan.config.ai.contextPrompt, language);
116
+ const markdown = await aiService.runGeneration(payload, prompt);
117
+ pagesByLang[language] = parseAiPages(markdown);
118
+ }
119
+
120
+ // Wire the AI pages into the latest documentation version (gitpagedocs pattern).
121
+ const versionId = DOC_VERSIONS[DOC_VERSIONS.length - 1] as string;
122
+ const result = await writeVersionDocs({ cwd, versionId, pagesByLang });
123
+
124
+ return {
125
+ scannedDirectories: scanned,
126
+ skippedDirectories: skipped,
127
+ scannedFilesCount: files.length,
128
+ outputs: result.files,
129
+ pages: result.slugs,
130
+ };
131
+ }
132
+
133
+ export async function runAiCliCommand(options: {
134
+ cwd: string;
135
+ onInfo?: (message: string) => void;
136
+ /** Provided by the composition root to scaffold the base gitpagedocs/ tree
137
+ * (so the version config.json exists before AI pages are wired into it). */
138
+ onScaffold?: () => Promise<void>;
139
+ }): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> {
140
+ const logInfo = options.onInfo ?? (() => undefined);
141
+ const configRepo = new AiConfigFileRepository({
142
+ cwd: options.cwd,
143
+ onMigrate: (fromPath, toPath) =>
144
+ logInfo(`[gitpagedocs:ai] Moved configuration from ${fromPath} to ${toPath} (secure user config directory).`),
145
+ });
146
+
147
+ const existingConfig = await configRepo.read();
148
+ const plan = await runAiInteractivePrompt(existingConfig);
149
+
150
+ if (plan.saveConfig) {
151
+ await configRepo.write(plan.config);
152
+ logInfo(`[gitpagedocs:ai] Configuration saved to ${configRepo.getConfigPath()}`);
153
+ }
154
+
155
+ // Build the base gitpagedocs structure BEFORE generation so the version
156
+ // config.json exists and the AI pages can be wired into it.
157
+ if (plan.runConfigScaffold && options.onScaffold) {
158
+ logInfo("[gitpagedocs] Generating base gitpagedocs structure...");
159
+ await options.onScaffold();
160
+ }
161
+
162
+ const summary = await executeAiRunPlan(plan, options.cwd);
163
+ return {
164
+ summary,
165
+ runConfigScaffold: plan.runConfigScaffold,
166
+ };
167
+ }
package/ai/config.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file config.ts
3
+ * @description Provider constants for the AI CLI. Co-located in cli/ so the
4
+ * published `gitpagedocs` package does not reach into the frontend app. The
5
+ * browser keeps its own copy at frontend/src/shared/config/ai-config.ts.
6
+ */
7
+
8
+ /** Legacy 4-provider id used by the interactive CLI (mapped to the 14-provider
9
+ * catalog in @gitpagedocs/tools via legacyProviderToCatalogId). */
10
+ export type AiProviderId = 'openai' | 'claude' | 'gemini' | 'ollama';
11
+
12
+ export const AI_MODEL_DEFAULTS: Readonly<Record<AiProviderId, string>> = {
13
+ openai: 'gpt-4o-mini',
14
+ claude: 'claude-3-5-sonnet-20240620',
15
+ gemini: 'gemini-1.5-flash',
16
+ ollama: 'llama3',
17
+ };
18
+
19
+ export const DEFAULT_AI_DOC_PROMPT =
20
+ 'You are a senior tech writer. Describe the provided code.';
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
+
50
+ export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434';
@@ -0,0 +1,44 @@
1
+ import type { AiMessage } from "@gitpagedocs/tools/ports";
2
+
3
+ /**
4
+ * @file chat-session.ts
5
+ * @description Pure conversation state for the interactive terminal chat. Holds
6
+ * the running message history and an optional system prompt, and builds the
7
+ * message array sent to a provider. No IO, no streaming, no terminal concerns —
8
+ * so the conversation logic is unit-testable without a provider or a TTY.
9
+ */
10
+ export class ChatSession {
11
+ private readonly history: AiMessage[] = [];
12
+
13
+ constructor(private readonly systemPrompt?: string) {}
14
+
15
+ /** Records a user turn. */
16
+ addUser(content: string): void {
17
+ this.history.push({ role: "user", content });
18
+ }
19
+
20
+ /** Records an assistant turn (the fully streamed reply). */
21
+ addAssistant(content: string): void {
22
+ this.history.push({ role: "assistant", content });
23
+ }
24
+
25
+ /** The user/assistant messages to send for the next completion. */
26
+ buildRequestMessages(): AiMessage[] {
27
+ return [...this.history];
28
+ }
29
+
30
+ /** The system prompt for the request's dedicated `system` field, if any. */
31
+ get system(): string | undefined {
32
+ return this.systemPrompt?.trim() || undefined;
33
+ }
34
+
35
+ /** Number of recorded turns (excluding the system prompt). */
36
+ get turnCount(): number {
37
+ return this.history.length;
38
+ }
39
+
40
+ /** Drops the conversation history, keeping the system prompt. */
41
+ clear(): void {
42
+ this.history.length = 0;
43
+ }
44
+ }
@@ -0,0 +1,33 @@
1
+ import type { AiProviderId } from "../../config";
2
+
3
+ export const AI_CLI_CONFIG_FILENAME = ".gitpagedocsconfig";
4
+
5
+ export interface AiCliConfig {
6
+ version: 1;
7
+ ai: {
8
+ provider: AiProviderId;
9
+ model: string;
10
+ apiKey?: string;
11
+ baseUrl?: string;
12
+ paths: string[];
13
+ languages: Array<"pt" | "en" | "es">;
14
+ outputDir: string;
15
+ filePrefix: string;
16
+ contextPrompt: string;
17
+ };
18
+ }
19
+
20
+ export interface AiCliRunPlan {
21
+ config: AiCliConfig;
22
+ saveConfig: boolean;
23
+ runConfigScaffold: boolean;
24
+ }
25
+
26
+ export interface AiCliRunSummary {
27
+ scannedDirectories: string[];
28
+ skippedDirectories: string[];
29
+ scannedFilesCount: number;
30
+ outputs: string[];
31
+ /** Slugs of the gitpagedocs pages wired into the version config (if any). */
32
+ pages?: string[];
33
+ }