@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.
- package/README.md +193 -0
- package/ai/application/ai-command.ts +24 -0
- package/ai/application/ai-provider-factory.ts +22 -0
- package/ai/application/docs-pattern.ts +72 -0
- package/ai/application/resolve-chat-credentials.ts +69 -0
- package/ai/application/run-ai-cli-command.ts +167 -0
- package/ai/config.ts +50 -0
- package/ai/core/chat-session.ts +44 -0
- package/ai/core/models/ai-cli-config.ts +33 -0
- package/ai/core/models/parse-ai-cli-config.ts +76 -0
- package/ai/core/ports/illm-provider.ts +32 -0
- package/ai/infrastructure/ai-config-file.ts +101 -0
- package/ai/infrastructure/file-system-adapter.ts +85 -0
- package/ai/infrastructure/llm/tools-llm-provider.ts +54 -0
- package/ai/infrastructure/version-docs-writer.ts +143 -0
- package/ai/presentation/ai-prompts.ts +239 -0
- package/application/config-only/handler.mjs +57 -0
- package/application/home/handler.mjs +87 -0
- package/application/ports/cli-runtime-ports.ts +93 -0
- package/application/report/config-only-reporter.mjs +68 -0
- package/application/use-cases/dispatch-mode.ts +18 -0
- package/builders/config-orchestrator.mjs +35 -0
- package/builders/language-bundles-builder.mjs +24 -0
- package/builders/project-links.mjs +50 -0
- package/builders/root-config-builder.d.mts +16 -0
- package/builders/root-config-builder.mjs +56 -0
- package/builders/route-builders.mjs +136 -0
- package/builders/theme-template.mjs +76 -0
- package/builders/version-config-builder.mjs +186 -0
- package/content/docs-en.mjs +556 -0
- package/content/docs-es.mjs +469 -0
- package/content/docs-pt.mjs +469 -0
- package/content/docs.mjs +9 -0
- package/contracts/doc-versions.mjs +3 -0
- package/contracts/index.mjs +11 -0
- package/contracts/langs-paths.mjs +28 -0
- package/contracts/languages.mjs +6 -0
- package/contracts/layouts-paths.d.mts +17 -0
- package/contracts/layouts-paths.mjs +68 -0
- package/contracts/path-segments.d.mts +2 -0
- package/contracts/path-segments.mjs +15 -0
- package/data/default-site-config.mjs +328 -0
- package/data/i18n-langmenu.mjs +350 -0
- package/data/i18n-translations.mjs +20 -0
- package/data/layouts.mjs +437 -0
- package/data/path-mappings.mjs +26 -0
- package/data/route-metas.mjs +93 -0
- package/data/theme-colors.mjs +444 -0
- package/data/urls.mjs +12 -0
- package/data/version-constants.mjs +3 -0
- package/domain/models/cli-options.ts +33 -0
- package/domain/services/layouts-migration.d.mts +15 -0
- package/domain/services/layouts-migration.mjs +74 -0
- package/domain/services/sanitize-segment.mjs +7 -0
- package/home/constants.mjs +4 -0
- package/home/home-file-writer.mjs +19 -0
- package/home/templates.mjs +51 -0
- package/index.mjs +38 -0
- package/infrastructure/config-only/runtime.mjs +17 -0
- package/infrastructure/gitpagedocs-config-file.ts +47 -0
- package/infrastructure/home/runtime.mjs +37 -0
- package/package.json +60 -0
- package/presentation/commands/ai-info.ts +57 -0
- package/presentation/commands/chat.ts +259 -0
- package/presentation/commands/config-clear.ts +23 -0
- package/presentation/commands/config-info.ts +72 -0
- package/presentation/commands/diagnostics.ts +174 -0
- package/presentation/commands/docs.ts +42 -0
- package/presentation/commands/mcp.ts +20 -0
- package/presentation/commands/pages.ts +103 -0
- package/presentation/commands/password.ts +62 -0
- package/presentation/commands/run-command.ts +72 -0
- package/presentation/index.ts +125 -0
- package/presentation/options/parser.ts +120 -0
- package/presentation/options/resolver.ts +46 -0
- package/presentation/options/schema.ts +14 -0
- package/presentation/ui/banner.ts +13 -0
- package/presentation/ui/clack.ts +91 -0
- package/presentation/ui/layouts-prompts.ts +124 -0
- package/presentation/ui/prompts.ts +147 -0
- package/presentation/ui/terminal-stream.ts +97 -0
- package/presentation/ui/tty.ts +37 -0
- package/runtime/doc-path-resolver.mjs +59 -0
- package/runtime/exec.mjs +168 -0
- package/runtime/git-ops.mjs +112 -0
- package/runtime/layouts-migrator.d.mts +11 -0
- package/runtime/layouts-migrator.mjs +117 -0
- package/runtime/output.mjs +201 -0
- package/runtime/workflow.mjs +131 -0
- package/start.mjs +14 -0
- 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
|
+
}
|