@gitpagedocs/cli 1.1.44

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 (66) hide show
  1. package/README.md +169 -0
  2. package/ai/application/ai-command.ts +24 -0
  3. package/ai/application/ai-provider-factory.ts +22 -0
  4. package/ai/application/run-ai-cli-command.ts +136 -0
  5. package/ai/config.ts +22 -0
  6. package/ai/core/models/ai-cli-config.ts +31 -0
  7. package/ai/core/ports/illm-provider.ts +32 -0
  8. package/ai/infrastructure/ai-config-file.ts +32 -0
  9. package/ai/infrastructure/file-system-adapter.ts +83 -0
  10. package/ai/infrastructure/llm/tools-llm-provider.ts +54 -0
  11. package/ai/presentation/ai-prompts.ts +239 -0
  12. package/application/config-only/handler.mjs +55 -0
  13. package/application/home/handler.mjs +83 -0
  14. package/application/ports/cli-runtime-ports.ts +87 -0
  15. package/application/report/config-only-reporter.mjs +62 -0
  16. package/application/use-cases/dispatch-mode.ts +18 -0
  17. package/builders/config-orchestrator.mjs +40 -0
  18. package/builders/root-config-builder.mjs +54 -0
  19. package/builders/route-builders.mjs +111 -0
  20. package/builders/source-viewer.mjs +330 -0
  21. package/builders/theme-template.mjs +75 -0
  22. package/builders/version-config-builder.mjs +151 -0
  23. package/content/docs-en.mjs +542 -0
  24. package/content/docs-es.mjs +459 -0
  25. package/content/docs-pt.mjs +459 -0
  26. package/content/docs.mjs +9 -0
  27. package/contracts/doc-versions.mjs +1 -0
  28. package/contracts/index.mjs +1 -0
  29. package/data/default-site-config.mjs +312 -0
  30. package/data/i18n-langmenu.mjs +122 -0
  31. package/data/i18n-translations.mjs +20 -0
  32. package/data/layouts.mjs +353 -0
  33. package/data/path-mappings.mjs +34 -0
  34. package/data/route-metas.mjs +73 -0
  35. package/data/theme-colors.mjs +387 -0
  36. package/data/urls.mjs +7 -0
  37. package/data/version-constants.mjs +3 -0
  38. package/domain/models/cli-options.ts +18 -0
  39. package/domain/services/sanitize-segment.mjs +7 -0
  40. package/home/constants.mjs +4 -0
  41. package/home/home-file-writer.mjs +20 -0
  42. package/home/templates.mjs +48 -0
  43. package/index.mjs +26 -0
  44. package/infrastructure/config-only/runtime.mjs +17 -0
  45. package/infrastructure/home/runtime.mjs +37 -0
  46. package/package.json +60 -0
  47. package/presentation/commands/ai-info.ts +57 -0
  48. package/presentation/commands/config-info.ts +24 -0
  49. package/presentation/commands/diagnostics.ts +70 -0
  50. package/presentation/commands/docs.ts +37 -0
  51. package/presentation/commands/mcp.ts +20 -0
  52. package/presentation/commands/pages.ts +91 -0
  53. package/presentation/commands/run-command.ts +62 -0
  54. package/presentation/index.ts +108 -0
  55. package/presentation/options/parser.ts +104 -0
  56. package/presentation/options/resolver.ts +25 -0
  57. package/presentation/options/schema.ts +10 -0
  58. package/presentation/ui/banner.ts +13 -0
  59. package/presentation/ui/clack.ts +74 -0
  60. package/presentation/ui/prompts.ts +64 -0
  61. package/runtime/doc-path-resolver.mjs +57 -0
  62. package/runtime/git-ops.mjs +110 -0
  63. package/runtime/output.mjs +122 -0
  64. package/runtime/workflow.mjs +144 -0
  65. package/start.mjs +12 -0
  66. package/ui/logger.mjs +9 -0
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # gitpagedocs (CLI)
2
+
3
+ The **published npm package** of the [Git Page Docs](../README.md) monorepo. 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 # global
7
+ gitpagedocs # generate gitpagedocs/ config (config-only)
8
+
9
+ # or one-off, no install:
10
+ npx gitpagedocs
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 `gitpagedocs/layouts/` |
21
+ | `gitpagedocs --push --owner <o> --repo <r> [--path <sub>]` | Generate, configure Pages, create workflow, commit + push |
22
+ | `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`: static site + .env + Dockerfile) |
23
+ | `gitpagedocs -i` / `--interactive` | Interactive prompts |
24
+ | `gitpagedocs ai` | Interactive AI documentation generator (see below) |
25
+ | `gitpagedocs provider [id]` · `models [provider]` | List AI providers / catalog models |
26
+ | `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI |
27
+ | `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push |
28
+ | `gitpagedocs docs` | Refresh managed regions of README/CONTRIBUTING/SECURITY |
29
+ | `gitpagedocs doctor` · `version` · `update` | Diagnostics / version / update hint |
30
+ | `gitpagedocs mcp start` | Start the MCP server over stdio |
31
+
32
+ The AI-CLI implementation lives in `cli/ai/` (relocated from the frontend so the published package is self-contained).
33
+
34
+ # CLI Architecture (Clean Architecture + SOLID)
35
+
36
+ ## Layer Overview
37
+
38
+ ```
39
+ cli/
40
+ ├── index.mjs # Node entry (bootstraps TS presentation via tsx)
41
+ ├── package.json # the published `gitpagedocs` package
42
+ ├── ai/ # AI documentation CLI (provider/model/paths → markdown)
43
+ ├── contracts/ # Stable contracts for external tooling
44
+ │ └── doc-versions.mjs
45
+ │
46
+ ├── presentation/ # Interface/composition root
47
+ │ ├── index.ts
48
+ │ ├── options/
49
+ │ └── ui/
50
+ │
51
+ ├── application/ # Use-cases and ports
52
+ │ ├── config-only/handler.mjs
53
+ │ ├── home/handler.mjs
54
+ │ ├── report/config-only-reporter.mjs
55
+ │ ├── use-cases/dispatch-mode.ts
56
+ │ └── ports/cli-runtime-ports.ts
57
+ │
58
+ ├── domain/ # Business rules
59
+ │ └── services/sanitize-segment.mjs
60
+ │
61
+ ├── infrastructure/ # Side effects and adapters
62
+ │ ├── config-only/runtime.mjs
63
+ │ └── home/runtime.mjs
64
+ │
65
+ ├── builders/ # Pure config builders
66
+ ├── data/ # Constants and metadata
67
+ ├── content/ # Static docs content
68
+ ├── runtime/ # Low-level runtime helpers
69
+ └── home/ # Home templates/files
70
+ ```
71
+
72
+ ## SOLID Principles
73
+
74
+ | Principle | Application |
75
+ |-----------|-------------|
76
+ | **SRP** | `application/report` only formats messages; handlers only orchestrate |
77
+ | **OCP** | New runtime implementations can be injected through ports |
78
+ | **LSP** | `dispatch-mode` works with any runner implementing `CliCommandRunner` |
79
+ | **ISP** | Split runtime contracts (`ConfigOnlyRuntimePort`, `HomeRuntimePort`) |
80
+ | **DIP** | Use-cases depend on `application/ports`, not concrete fs/git/exec |
81
+
82
+ ## Clean Architecture
83
+
84
+ - **Interfaces/Presentation**: parse CLI args, resolve mode, wire dependencies in `presentation/index.ts`
85
+ - **Application**: orchestrate flows with explicit ports, without direct fs/git/exec imports
86
+ - **Domain**: sanitize and core rules with no infrastructure dependencies
87
+ - **Infrastructure**: concrete adapters for runtime side effects
88
+ - **Contracts**: `tools/smoke` consumes `cli/contracts` instead of internal CLI modules
89
+
90
+ ## Authorized Routes Pipeline
91
+
92
+ The CLI now generates authorized-route docs and config entries in PT/EN/ES.
93
+
94
+ - Source content: `cli/content/docs.mjs` (`authorizedRoutes` key per language)
95
+ - Route metadata: `cli/data/route-metas.mjs` (`ROUTE_META_ID6`)
96
+ - Route path mapping: `cli/data/path-mappings.mjs` (`authorized-routes.md`)
97
+ - Version config composition: `cli/builders/version-config-builder.mjs`
98
+ - Doc key resolution: `cli/runtime/doc-path-resolver.mjs`
99
+
100
+ Generated artifacts include:
101
+
102
+ - `gitpagedocs/docs/versions/<version>/config.json` with `auth` and route `authorization`
103
+ - `gitpagedocs/docs/versions/<version>/{pt,en,es}/authorized-routes.md`
104
+ - `menus-header-md` entry dedicated to Authorized Routes
105
+
106
+ ## GitHub Pages Push Paths
107
+
108
+ - Default project-site publish (`--push` without `--path`) targets `/<repo>/`.
109
+ - Custom project subpath publish (`--push --path <segment>`) targets `/<repo>/<segment>/`.
110
+ - `--path` accepts a single sanitized segment (letters, numbers, `.`, `_`, `-`).
111
+
112
+ Example:
113
+
114
+ ```bash
115
+ npx gitpagedocs --owner vidigal-code --repo energy-bill-ai-parser --path git-docs --push
116
+ ```
117
+
118
+ Expected URL after deployment:
119
+
120
+ ```text
121
+ https://vidigal-code.github.io/energy-bill-ai-parser/git-docs/
122
+ ```
123
+
124
+ ## AI CLI (interactive + config file)
125
+
126
+ The CLI now includes a dedicated interactive AI mode:
127
+
128
+ ```bash
129
+ npx gitpagedocs ai
130
+ ```
131
+
132
+ ### What it does
133
+
134
+ - asks provider (`openai`, `claude`, `gemini`, `ollama`)
135
+ - asks API key or Ollama URL
136
+ - asks paths to scan (supports one or many paths, including other repositories)
137
+ - generates markdown documentation in `pt`, `en`, `es`
138
+ - optionally persists config in `.gitpagedocsconfig`
139
+ - optionally runs standard `gitpagedocs` scaffolding after AI generation
140
+
141
+ ### `.gitpagedocsconfig` (manual mode)
142
+
143
+ You can create/edit this file manually and then run `npx gitpagedocs ai`:
144
+
145
+ ```json
146
+ {
147
+ "version": 1,
148
+ "ai": {
149
+ "provider": "openai",
150
+ "model": "gpt-4o-mini",
151
+ "apiKey": "<YOUR_API_KEY>",
152
+ "paths": ["src", "cli", "../another-repo/src"],
153
+ "languages": ["pt", "en", "es"],
154
+ "outputDir": "gitpagedocs/docs",
155
+ "filePrefix": "ai-generated",
156
+ "contextPrompt": "Você é um redator técnico sênior..."
157
+ }
158
+ }
159
+ ```
160
+
161
+ For Ollama, use `baseUrl` instead of `apiKey`.
162
+
163
+ ### Interactive fallback
164
+
165
+ If a directory is not found, CLI offers fallback choices:
166
+
167
+ - fix paths and retry
168
+ - skip missing paths and continue
169
+ - 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 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,136 @@
1
+ import path from "node:path";
2
+ import { AiCommandService } from "./ai-command";
3
+ import { createAiProvider } from "./ai-provider-factory";
4
+ import { FileSystemAdapter, type FilePayload } from "../infrastructure/file-system-adapter";
5
+ import {
6
+ AiConfigFileRepository,
7
+ } from "../infrastructure/ai-config-file";
8
+ import type { AiCliRunPlan, AiCliRunSummary } from "../core/models/ai-cli-config";
9
+ import { promptMissingDirectories, runAiInteractivePrompt } from "../presentation/ai-prompts";
10
+
11
+ const LANGUAGE_TO_LABEL: Record<string, string> = {
12
+ pt: "Portuguese",
13
+ en: "English",
14
+ es: "Español",
15
+ };
16
+
17
+ function aggregateFilesPayload(files: FilePayload[]): string {
18
+ return files
19
+ .map((file) => `\n\n--- FILE: ${file.filePath} ---\n\n${file.content}`)
20
+ .join("");
21
+ }
22
+
23
+ function buildLanguagePrompt(basePrompt: string, language: "pt" | "en" | "es"): string {
24
+ 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.`;
26
+ }
27
+
28
+ async function collectFilesWithInteractiveFallback(
29
+ fileSystem: FileSystemAdapter,
30
+ initialPaths: string[],
31
+ ): Promise<{ files: FilePayload[]; scanned: string[]; skipped: string[] }> {
32
+ let queue = [...initialPaths];
33
+ let scanned: string[] = [];
34
+ let skipped: string[] = [];
35
+ const files: FilePayload[] = [];
36
+
37
+ while (queue.length > 0) {
38
+ const { existing, missing } = fileSystem.splitExistingDirectories(queue);
39
+ scanned = [...scanned, ...existing];
40
+
41
+ for (const directory of existing) {
42
+ const directoryFiles = await fileSystem.readDirectoryRecursively(directory);
43
+ files.push(...directoryFiles);
44
+ }
45
+
46
+ if (!missing.length) break;
47
+
48
+ const fallback = await promptMissingDirectories(missing);
49
+ if (fallback.abort) {
50
+ skipped = [...skipped, ...missing];
51
+ break;
52
+ }
53
+
54
+ if (!fallback.replacementPaths.length) {
55
+ skipped = [...skipped, ...missing];
56
+ break;
57
+ }
58
+
59
+ queue = fallback.replacementPaths;
60
+ }
61
+
62
+ return {
63
+ files,
64
+ scanned,
65
+ skipped,
66
+ };
67
+ }
68
+
69
+ async function runPlan(plan: AiCliRunPlan): Promise<AiCliRunSummary> {
70
+ const provider = createAiProvider({
71
+ provider: plan.config.ai.provider,
72
+ model: plan.config.ai.model,
73
+ apiKey: plan.config.ai.apiKey,
74
+ baseUrl: plan.config.ai.baseUrl,
75
+ });
76
+
77
+ const aiService = new AiCommandService(provider);
78
+ const fileSystem = new FileSystemAdapter();
79
+
80
+ const { files, scanned, skipped } = await collectFilesWithInteractiveFallback(
81
+ fileSystem,
82
+ plan.config.ai.paths,
83
+ );
84
+
85
+ if (!files.length) {
86
+ return {
87
+ scannedDirectories: scanned,
88
+ skippedDirectories: skipped,
89
+ scannedFilesCount: 0,
90
+ outputs: [],
91
+ };
92
+ }
93
+
94
+ const payload = aggregateFilesPayload(files);
95
+ const outputs: string[] = [];
96
+
97
+ for (const language of plan.config.ai.languages) {
98
+ const prompt = buildLanguagePrompt(plan.config.ai.contextPrompt, language);
99
+ 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);
106
+ }
107
+
108
+ return {
109
+ scannedDirectories: scanned,
110
+ skippedDirectories: skipped,
111
+ scannedFilesCount: files.length,
112
+ outputs,
113
+ };
114
+ }
115
+
116
+ export async function runAiCliCommand(options: {
117
+ cwd: string;
118
+ onInfo?: (message: string) => void;
119
+ }): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> {
120
+ const logInfo = options.onInfo ?? (() => undefined);
121
+ const configRepo = new AiConfigFileRepository(options.cwd);
122
+
123
+ const existingConfig = await configRepo.read();
124
+ const plan = await runAiInteractivePrompt(existingConfig);
125
+
126
+ if (plan.saveConfig) {
127
+ await configRepo.write(plan.config);
128
+ logInfo(`[gitpagedocs:ai] Configuration saved to ${configRepo.getConfigPath()}`);
129
+ }
130
+
131
+ const summary = await runPlan(plan);
132
+ return {
133
+ summary,
134
+ runConfigScaffold: plan.runConfigScaffold,
135
+ };
136
+ }
package/ai/config.ts ADDED
@@ -0,0 +1,22 @@
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
+ export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434';
@@ -0,0 +1,31 @@
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
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file illm-provider.ts
3
+ * @description Contract for all LLM implementations ensuring OCP and DIP.
4
+ * Isolates the AI logic from the specific LLM providers (OpenAI, Claude, Ollama, etc.).
5
+ */
6
+
7
+ export interface ChatMessage {
8
+ role: 'user' | 'assistant' | 'system';
9
+ content: string;
10
+ type?: 'text' | 'image' | 'audio'; // Supporting multimedia as requested
11
+ }
12
+
13
+ export interface ILLMProviderConfig {
14
+ apiKey?: string;
15
+ baseUrl?: string;
16
+ model?: string;
17
+ }
18
+
19
+ export interface ILLMProvider {
20
+ /**
21
+ * Generates markdown documentation based on file content.
22
+ * @param fileContent Raw content of the source file
23
+ * @param contextPrompt Optional instructions to guide the generation (e.g., FSD standard)
24
+ */
25
+ generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string>;
26
+
27
+ /**
28
+ * Conversational chat support.
29
+ * @param messages Array of chat messages (system context, user history, etc.)
30
+ */
31
+ chat(messages: ChatMessage[]): Promise<string>;
32
+ }
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ AI_CLI_CONFIG_FILENAME,
5
+ type AiCliConfig,
6
+ } from "../core/models/ai-cli-config";
7
+
8
+ export class AiConfigFileRepository {
9
+ constructor(private readonly cwd: string = process.cwd()) {}
10
+
11
+ getConfigPath(): string {
12
+ return path.join(this.cwd, AI_CLI_CONFIG_FILENAME);
13
+ }
14
+
15
+ async read(): Promise<AiCliConfig | null> {
16
+ const configPath = this.getConfigPath();
17
+ try {
18
+ const content = await fs.readFile(configPath, "utf-8");
19
+ const parsed = JSON.parse(content) as Partial<AiCliConfig>;
20
+ if (!parsed || parsed.version !== 1 || !parsed.ai) return null;
21
+ return parsed as AiCliConfig;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ async write(config: AiCliConfig): Promise<void> {
28
+ const configPath = this.getConfigPath();
29
+ const data = JSON.stringify(config, null, 2);
30
+ await fs.writeFile(configPath, `${data}\n`, "utf-8");
31
+ }
32
+ }
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { statSync } from 'node:fs';
4
+
5
+ export interface FilePayload {
6
+ filePath: string;
7
+ content: string;
8
+ }
9
+
10
+ export class FileSystemAdapter {
11
+ splitExistingDirectories(targetDirectories: string[]): { existing: string[]; missing: string[] } {
12
+ const existing: string[] = [];
13
+ const missing: string[] = [];
14
+
15
+ for (const targetDirectory of targetDirectories) {
16
+ const absolutePath = path.resolve(process.cwd(), targetDirectory);
17
+ try {
18
+ const stat = statSync(absolutePath);
19
+ if (stat.isDirectory()) {
20
+ existing.push(targetDirectory);
21
+ } else {
22
+ missing.push(targetDirectory);
23
+ }
24
+ } catch {
25
+ missing.push(targetDirectory);
26
+ }
27
+ }
28
+
29
+ return { existing, missing };
30
+ }
31
+
32
+ async readDirectoryRecursively(targetDirectory: string): Promise<FilePayload[]> {
33
+ const absolutePath = path.resolve(process.cwd(), targetDirectory);
34
+ const resolvedFiles: FilePayload[] = [];
35
+
36
+ try {
37
+ await this.traverse(absolutePath, resolvedFiles);
38
+ } catch (error) {
39
+ }
40
+
41
+ return resolvedFiles;
42
+ }
43
+
44
+ private async traverse(currentPath: string, payloadList: FilePayload[]): Promise<void> {
45
+ let stats;
46
+ try {
47
+ stats = await fs.stat(currentPath);
48
+ } catch {
49
+ return;
50
+ }
51
+
52
+ if (stats.isDirectory()) {
53
+ const basename = path.basename(currentPath);
54
+ if (['node_modules', '.git', '.next', 'out', 'dist'].includes(basename)) {
55
+ return;
56
+ }
57
+
58
+ const entries = await fs.readdir(currentPath);
59
+ for (const entry of entries) {
60
+ await this.traverse(path.join(currentPath, entry), payloadList);
61
+ }
62
+ } else if (stats.isFile()) {
63
+ const ext = path.extname(currentPath);
64
+ const validExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.css'];
65
+ if (validExtensions.includes(ext)) {
66
+ try {
67
+ const content = await fs.readFile(currentPath, 'utf-8');
68
+ payloadList.push({
69
+ filePath: currentPath,
70
+ content: content
71
+ });
72
+ } catch {
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ async writeDocumentationOutput(outputContent: string, targetPath: string = "documentation-output.md"): Promise<void> {
79
+ const absoluteDest = path.resolve(process.cwd(), targetPath);
80
+ await fs.mkdir(path.dirname(absoluteDest), { recursive: true });
81
+ await fs.writeFile(absoluteDest, outputContent, 'utf-8');
82
+ }
83
+ }
@@ -0,0 +1,54 @@
1
+ import { createDefaultFactory, legacyProviderToCatalogId } from "@gitpagedocs/tools/ai";
2
+ import type { AiMessage, ProviderConfig } from "@gitpagedocs/tools/ports";
3
+ import type { ILLMProvider, ChatMessage, ILLMProviderConfig } from "../../core/ports/illm-provider";
4
+ import { DEFAULT_AI_DOC_PROMPT, type AiProviderId } from "../../config";
5
+
6
+ const factory = createDefaultFactory();
7
+
8
+ /**
9
+ * Single CLI LLM provider backed by the shared @gitpagedocs/tools AI core.
10
+ * Replaces the four duplicated CLI provider classes (OpenAI/Claude/Gemini/
11
+ * Ollama) with one adapter over the registry/factory, preserving the
12
+ * ILLMProvider contract used by the AI documentation flow.
13
+ */
14
+ export class ToolsLlmProvider implements ILLMProvider {
15
+ private readonly providerId;
16
+ private readonly model: string;
17
+ private readonly apiKey?: string;
18
+ private readonly baseUrl?: string;
19
+
20
+ constructor(legacyProvider: AiProviderId, config: ILLMProviderConfig) {
21
+ this.providerId = legacyProviderToCatalogId(legacyProvider);
22
+ this.model = config.model ?? "";
23
+ this.apiKey = config.apiKey;
24
+ this.baseUrl = config.baseUrl;
25
+ }
26
+
27
+ async generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string> {
28
+ return this.chat([
29
+ { role: "system", content: contextPrompt || DEFAULT_AI_DOC_PROMPT },
30
+ { role: "user", content: fileContent },
31
+ ]);
32
+ }
33
+
34
+ async chat(messages: ChatMessage[]): Promise<string> {
35
+ const system = messages
36
+ .filter((m) => m.role === "system")
37
+ .map((m) => m.content)
38
+ .join("\n");
39
+ const aiMessages: AiMessage[] = messages
40
+ .filter((m) => m.role !== "system")
41
+ .map((m) => ({ role: m.role as AiMessage["role"], content: m.content }));
42
+
43
+ const config: ProviderConfig = {
44
+ providerId: this.providerId,
45
+ model: this.model,
46
+ apiKey: this.apiKey,
47
+ baseUrl: this.baseUrl,
48
+ };
49
+ const response = await factory
50
+ .create(this.providerId)
51
+ .generate({ messages: aiMessages, system: system || undefined, maxTokens: 4000 }, config);
52
+ return response.text;
53
+ }
54
+ }