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