@gitpagedocs/cli 0.0.2 → 0.0.4
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 +4 -5
- package/ai/application/resolve-chat-credentials.ts +1 -2
- package/ai/application/run-ai-cli-command.ts +1 -5
- package/ai/infrastructure/ai-config-file.ts +9 -42
- package/builders/theme-template.mjs +17 -7
- package/content/docs-en.mjs +6 -4
- package/content/docs-es.mjs +5 -2
- package/content/docs-pt.mjs +5 -2
- package/contracts/doc-versions.mjs +1 -1
- package/contracts/index.mjs +0 -2
- package/contracts/langs-paths.mjs +4 -7
- package/contracts/languages.mjs +3 -3
- package/contracts/layouts-paths.d.mts +0 -2
- package/contracts/layouts-paths.mjs +1 -16
- package/data/i18n-langmenu.mjs +60 -12
- package/data/urls.mjs +1 -6
- package/package.json +3 -3
- package/presentation/commands/chat.ts +1 -2
- package/presentation/commands/config-clear.ts +4 -4
- package/presentation/commands/config-info.ts +5 -24
- package/presentation/index.ts +0 -7
- package/presentation/ui/layouts-prompts.ts +3 -91
- package/runtime/output.mjs +6 -44
- package/domain/services/layouts-migration.d.mts +0 -15
- package/domain/services/layouts-migration.mjs +0 -74
- package/runtime/layouts-migrator.d.mts +0 -11
- package/runtime/layouts-migrator.mjs +0 -117
package/README.md
CHANGED
|
@@ -169,9 +169,8 @@ permissions on POSIX systems:
|
|
|
169
169
|
- Linux: `$XDG_CONFIG_HOME/gitpagedocs/.gitpagedocsconfig` (or `~/.config/gitpagedocs/.gitpagedocsconfig`)
|
|
170
170
|
|
|
171
171
|
Set `GITPAGEDOCS_CONFIG_DIR` to override the directory; delete the stored file,
|
|
172
|
-
the vault and their credentials with `gitpagedocs config clear
|
|
173
|
-
|
|
174
|
-
run. You can create/edit this file manually and then run `npx @gitpagedocs/cli ai`:
|
|
172
|
+
the vault and their credentials with `gitpagedocs config clear`. You can create/edit
|
|
173
|
+
this file manually and then run `npx @gitpagedocs/cli ai`:
|
|
175
174
|
|
|
176
175
|
```json
|
|
177
176
|
{
|
|
@@ -200,8 +199,8 @@ derived from your password with PBKDF2-HMAC-SHA-256, 210k iterations) and the co
|
|
|
200
199
|
(3 attempts, then the run aborts with `Vault password rejected`).
|
|
201
200
|
- `GITPAGEDOCS_VAULT_PASSWORD` supplies the password without a prompt (CI, pipes, no TTY);
|
|
202
201
|
on a first run it initializes the vault, otherwise it must match the existing one.
|
|
203
|
-
- A
|
|
204
|
-
|
|
202
|
+
- A plaintext `"apiKey"` written by 0.0.1 is sealed into the vault and removed from the file
|
|
203
|
+
the next time the config is read.
|
|
205
204
|
- If you hand-write the file, leave the key out: enter it in the `gitpagedocs ai` prompt (it is
|
|
206
205
|
sealed on save). `gitpagedocs chat` can also read the provider's environment variable
|
|
207
206
|
(`gitpagedocs provider <id>` lists it) when no key is stored.
|
|
@@ -13,7 +13,6 @@ export interface ResolvedChatCredentials {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface ResolveChatCredentialsInput {
|
|
16
|
-
readonly cwd: string;
|
|
17
16
|
/** Overrides the stored/detected provider (e.g. a `--provider` flag). */
|
|
18
17
|
readonly providerOverride?: string;
|
|
19
18
|
/** Overrides the stored/detected model (e.g. a `--model` flag). */
|
|
@@ -40,7 +39,7 @@ export async function resolveChatCredentials(
|
|
|
40
39
|
input: ResolveChatCredentialsInput,
|
|
41
40
|
): Promise<ResolvedChatCredentials | null> {
|
|
42
41
|
const env = input.env ?? process.env;
|
|
43
|
-
const configRepo = input.configRepo ?? new AiConfigFileRepository(
|
|
42
|
+
const configRepo = input.configRepo ?? new AiConfigFileRepository();
|
|
44
43
|
const stored = await configRepo.read().catch(() => null);
|
|
45
44
|
|
|
46
45
|
const providerId = resolveProviderId(input.providerOverride, stored, env);
|
|
@@ -172,11 +172,7 @@ async function saveConfig(config: AiCliConfig, deps: SaveConfigDeps): Promise<vo
|
|
|
172
172
|
|
|
173
173
|
export async function runAiCliCommand(options: RunAiCliCommandOptions): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> {
|
|
174
174
|
const logInfo = options.onInfo ?? (() => undefined);
|
|
175
|
-
const configRepo = new AiConfigFileRepository(
|
|
176
|
-
cwd: options.cwd,
|
|
177
|
-
onMigrate: (fromPath, toPath) =>
|
|
178
|
-
logInfo(`[gitpagedocs:ai] Moved configuration from ${fromPath} to ${toPath} (secure user config directory).`),
|
|
179
|
-
});
|
|
175
|
+
const configRepo = new AiConfigFileRepository();
|
|
180
176
|
const vault = options.vault ?? new AiKeyVault();
|
|
181
177
|
const prompt = options.passwordPrompt ?? clackVaultPasswordPrompt;
|
|
182
178
|
|
|
@@ -12,12 +12,8 @@ const CONFIG_DIR_MODE = 0o700;
|
|
|
12
12
|
const CONFIG_FILE_MODE = 0o600;
|
|
13
13
|
|
|
14
14
|
export interface AiConfigFileRepositoryOptions {
|
|
15
|
-
/** Working directory searched for a legacy `<cwd>/.gitpagedocsconfig`. */
|
|
16
|
-
cwd?: string;
|
|
17
15
|
/** Destination directory; defaults to the per-user OS config directory. */
|
|
18
16
|
configDir?: string;
|
|
19
|
-
/** Notified after a legacy config is moved into the user config directory. */
|
|
20
|
-
onMigrate?: (fromPath: string, toPath: string) => void;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
/**
|
|
@@ -25,47 +21,32 @@ export interface AiConfigFileRepositoryOptions {
|
|
|
25
21
|
* (`%APPDATA%\gitpagedocs` on Windows, `~/Library/Application Support/gitpagedocs`
|
|
26
22
|
* on macOS, `$XDG_CONFIG_HOME/gitpagedocs` elsewhere) with owner-only
|
|
27
23
|
* permissions, so the API key never lives inside a repository checkout.
|
|
28
|
-
* A legacy `<cwd>/.gitpagedocsconfig` is migrated automatically on first read.
|
|
29
24
|
*/
|
|
30
25
|
export class AiConfigFileRepository {
|
|
31
|
-
private readonly cwd: string;
|
|
32
26
|
private readonly configDir: string;
|
|
33
|
-
private readonly onMigrate?: (fromPath: string, toPath: string) => void;
|
|
34
27
|
|
|
35
28
|
constructor(options: AiConfigFileRepositoryOptions = {}) {
|
|
36
|
-
this.cwd = options.cwd ?? process.cwd();
|
|
37
29
|
this.configDir = options.configDir ?? resolveUserConfigDir();
|
|
38
|
-
this.onMigrate = options.onMigrate;
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
getConfigPath(): string {
|
|
42
33
|
return path.join(this.configDir, AI_CLI_CONFIG_FILENAME);
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
getLegacyConfigPath(): string {
|
|
46
|
-
return path.join(this.cwd, AI_CLI_CONFIG_FILENAME);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
36
|
async read(): Promise<AiCliConfig | null> {
|
|
50
|
-
|
|
51
|
-
if (current) return current;
|
|
52
|
-
return this.migrateLegacyConfig();
|
|
37
|
+
return this.readFrom(this.getConfigPath());
|
|
53
38
|
}
|
|
54
39
|
|
|
55
|
-
/** Delete the stored configuration
|
|
56
|
-
* any saved credentials. Returns the paths that were actually removed. */
|
|
40
|
+
/** Delete the stored configuration, wiping any saved settings. Returns the removed path, if any. */
|
|
57
41
|
async clear(): Promise<string[]> {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Missing file: nothing to remove at this location.
|
|
66
|
-
}
|
|
42
|
+
const configPath = path.resolve(this.getConfigPath());
|
|
43
|
+
try {
|
|
44
|
+
await fs.rm(configPath);
|
|
45
|
+
return [configPath];
|
|
46
|
+
} catch {
|
|
47
|
+
// Missing file: nothing to remove.
|
|
48
|
+
return [];
|
|
67
49
|
}
|
|
68
|
-
return removed;
|
|
69
50
|
}
|
|
70
51
|
|
|
71
52
|
async write(config: AiCliConfig): Promise<void> {
|
|
@@ -84,18 +65,4 @@ export class AiConfigFileRepository {
|
|
|
84
65
|
}
|
|
85
66
|
}
|
|
86
67
|
|
|
87
|
-
private async migrateLegacyConfig(): Promise<AiCliConfig | null> {
|
|
88
|
-
const legacyPath = this.getLegacyConfigPath();
|
|
89
|
-
if (path.resolve(legacyPath) === path.resolve(this.getConfigPath())) return null;
|
|
90
|
-
const legacy = await this.readFrom(legacyPath);
|
|
91
|
-
if (!legacy) return null;
|
|
92
|
-
try {
|
|
93
|
-
await this.write(legacy);
|
|
94
|
-
await fs.rm(legacyPath, { force: true });
|
|
95
|
-
this.onMigrate?.(legacyPath, this.getConfigPath());
|
|
96
|
-
} catch {
|
|
97
|
-
// Migration is best-effort: keep serving the legacy config on failure.
|
|
98
|
-
}
|
|
99
|
-
return legacy;
|
|
100
|
-
}
|
|
101
68
|
}
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
import { PACKAGE_VERSION } from "../contracts/doc-versions.mjs";
|
|
4
4
|
import { THEME_COLORS } from "../data/theme-colors.mjs";
|
|
5
5
|
|
|
6
|
+
/** `#RRGGBB` (or `#RGB`) to `rgba(r, g, b, alpha)` for palette-derived glows and borders. */
|
|
7
|
+
function rgba(hex, alpha) {
|
|
8
|
+
const digits = String(hex).replace("#", "");
|
|
9
|
+
const full = digits.length === 3 ? digits.replace(/./g, (c) => c + c) : digits.slice(0, 6);
|
|
10
|
+
const value = Number.parseInt(full, 16);
|
|
11
|
+
return `rgba(${(value >> 16) & 255}, ${(value >> 8) & 255}, ${value & 255}, ${alpha})`;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export function createThemeTemplate(layout, themeColors = THEME_COLORS) {
|
|
7
15
|
const colors = themeColors[layout.id] ?? themeColors.default;
|
|
8
16
|
const dark = layout.mode === "dark";
|
|
@@ -40,22 +48,24 @@ export function createThemeTemplate(layout, themeColors = THEME_COLORS) {
|
|
|
40
48
|
padding: "24px",
|
|
41
49
|
boxShadow: dark ? "0 18px 60px rgba(0, 0, 0, 0.45)" : "0 18px 50px rgba(15, 23, 42, 0.08)",
|
|
42
50
|
},
|
|
51
|
+
// Controls follow the layout's own palette (card surface, card border,
|
|
52
|
+
// primary accent) so a green or amber theme never gets a slate select.
|
|
43
53
|
button: {
|
|
44
54
|
borderRadius: "12px",
|
|
45
55
|
padding: "10px 18px",
|
|
46
|
-
border:
|
|
47
|
-
hoverGlow:
|
|
56
|
+
border: `1px solid ${colors.cardBorder}`,
|
|
57
|
+
hoverGlow: `0 0 0 3px ${rgba(colors.primary, 0.18)}`,
|
|
48
58
|
},
|
|
49
59
|
select: {
|
|
50
60
|
borderRadius: "12px",
|
|
51
61
|
padding: "10px 40px 10px 16px",
|
|
52
|
-
border:
|
|
53
|
-
backgroundColor:
|
|
62
|
+
border: `1px solid ${colors.cardBorder}`,
|
|
63
|
+
backgroundColor: colors.cardBackground,
|
|
54
64
|
textAlign: "center",
|
|
55
65
|
iconColor: colors.secondary,
|
|
56
|
-
hoverBorderColor:
|
|
57
|
-
focusBorderColor:
|
|
58
|
-
focusGlow:
|
|
66
|
+
hoverBorderColor: rgba(colors.primary, 0.7),
|
|
67
|
+
focusBorderColor: rgba(colors.primary, 0.85),
|
|
68
|
+
focusGlow: `0 0 0 4px ${rgba(colors.primary, 0.18)}`,
|
|
59
69
|
},
|
|
60
70
|
checkbox: {
|
|
61
71
|
width: "20px",
|
package/content/docs-en.mjs
CHANGED
|
@@ -188,7 +188,7 @@ The docs ship an AI assistant in two surfaces: an in-docs **chat drawer** (the A
|
|
|
188
188
|
|
|
189
189
|
- **14 providers** via one shared core: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity.
|
|
190
190
|
- **Model selection** — pick from each provider's catalog (\`gitpagedocs models <provider>\`); a stored model id the provider has retired is replaced by the provider default automatically.
|
|
191
|
-
- **Encrypted at rest** — your API key is sealed with AES-256-GCM behind a **local password** and is never stored in plaintext or logged.
|
|
191
|
+
- **Encrypted at rest** — your API key is sealed with AES-256-GCM behind a **local password** and is never stored in plaintext or logged.
|
|
192
192
|
- **Inactivity auto-lock** — the chat drawer locks itself after \`site.AiChatAutoLockSeconds\` idle seconds (default 30, \`0\` disables): a centered countdown modal in your language lets you cancel or lock now, and unlocking asks for the password again.
|
|
193
193
|
- **Resilient providers** — transient provider errors are retried (3 attempts) and a final failure is a plain message ending with "Try again!".
|
|
194
194
|
- **AI documentation generation** — \`gitpagedocs ai\` scans chosen paths and writes multilingual markdown (pt/en/es); reusable via \`.gitpagedocsconfig\`, whose API key lives sealed in the encrypted \`.gitpagedocsvault\` (the vault password is asked on every run). \`gitpagedocs chat\` brings the same assistant to the terminal.
|
|
@@ -325,7 +325,7 @@ Runtime configuration lives in \`gitpagedocs/config.json\`. UI text lives next t
|
|
|
325
325
|
|
|
326
326
|
- \`site.languages\` in \`gitpagedocs/config.json\` switches each language on (\`true\`) or off (\`false\`), in menu order: \`{ "languages": { "en": true, "pt": true, "es": false } }\`. A language set to \`false\` disappears from the language selector and its strings are not loaded, even when its docs exist.
|
|
327
327
|
- \`gitpagedocs/langs/<lang>.json\` holds that language's strings: \`langmenu\` (header, search, source viewer, audio player, AI chat and docs-access labels) and \`translations\` (\`notFound\`, \`navigation\`, \`footer\`).
|
|
328
|
-
- To add a language, create \`langs/<lang>.json\` and add \`"<lang>": true\` to \`site.languages\`.
|
|
328
|
+
- To add a language, create \`langs/<lang>.json\` and add \`"<lang>": true\` to \`site.languages\`.
|
|
329
329
|
- Older \`config.json\` files that still inline \`site.langmenu\` / \`translations\` keep working; when both exist, the \`langs/\` files win and any missing key is backfilled from the current release.
|
|
330
330
|
|
|
331
331
|
## \`site\` section
|
|
@@ -391,6 +391,10 @@ Version configs support multiple content types:
|
|
|
391
391
|
|
|
392
392
|
Each route can include \`title\`, \`description\` (per language), \`titleCss\`, \`titlePosition: "center"\`, \`descriptionPosition: "center"\`, \`titleIsVisible\`, \`descriptionIsVisible\`.
|
|
393
393
|
|
|
394
|
+
Settings every route shares are written once in \`routeDefaults\` at the top of the version config; each route keeps only its own values, and a value written on a route wins over \`routeDefaults\`.
|
|
395
|
+
|
|
396
|
+
Header icons live in \`site.icons\`: \`defaults\` holds what all icons share (\`reactIcon\`, \`colorDark\`, \`colorLight\`, \`size\`, \`imgDark\`, \`imgLight\`, \`imgWidth\`, \`imgHeight\`) and each icon entry sets its \`tag\` plus any difference (\`null\` removes a field). The older flat keys (\`IconNavMenuOpenReactIconesTag\`, …) still work and win when both are present.
|
|
397
|
+
|
|
394
398
|
## Route-level variables (blockLink, container, url, browseAll)
|
|
395
399
|
|
|
396
400
|
Per-route options for \`routes-md\`, \`routes-html\`, \`routes-video\`, and \`routes-audio\`:
|
|
@@ -505,8 +509,6 @@ Themes are JSON templates mapped by \`layoutsConfig.json\`.
|
|
|
505
509
|
- \`gitpagelayouts/layoutsFallbackConfig.json\`
|
|
506
510
|
- \`gitpagelayouts/templates/*.json\`
|
|
507
511
|
|
|
508
|
-
The legacy \`gitpagedocs/layouts/\` location is no longer generated. It is still read by the viewer, and the CLI offers to move it into the layouts home - only after you confirm.
|
|
509
|
-
|
|
510
512
|
## Template model
|
|
511
513
|
|
|
512
514
|
Each template usually includes:
|
package/content/docs-es.mjs
CHANGED
|
@@ -172,7 +172,7 @@ Los docs traen un asistente de IA en dos superficies: un **panel de chat** dentr
|
|
|
172
172
|
|
|
173
173
|
- **14 proveedores** en un unico core compartido: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity.
|
|
174
174
|
- **Eleccion de modelo** — desde el catalogo de cada proveedor (\`gitpagedocs models <proveedor>\`); un id de modelo guardado que el proveedor retiro se reemplaza por el predeterminado del proveedor automaticamente.
|
|
175
|
-
- **Cifrado en reposo** — tu clave de API se sella con AES-256-GCM detras de una **contrasena local** y nunca queda en texto plano ni en logs.
|
|
175
|
+
- **Cifrado en reposo** — tu clave de API se sella con AES-256-GCM detras de una **contrasena local** y nunca queda en texto plano ni en logs.
|
|
176
176
|
- **Bloqueo por inactividad** — el panel de chat se bloquea solo tras \`site.AiChatAutoLockSeconds\` segundos sin uso (por defecto 30, \`0\` lo desactiva): un modal centrado con cuenta regresiva, en tu idioma, permite cancelar o bloquear ahora, y desbloquear pide la contrasena de nuevo.
|
|
177
177
|
- **Proveedores resilientes** — los errores transitorios del proveedor se reintentan (3 intentos) y un fallo final es un mensaje simple que termina en "¡Inténtalo de nuevo!".
|
|
178
178
|
- **Generacion de documentacion con IA** — \`gitpagedocs ai\` recorre las rutas elegidas y escribe markdown multilingue (pt/en/es); reutilizable via \`.gitpagedocsconfig\`, cuya clave de API queda sellada en la boveda cifrada \`.gitpagedocsvault\` (la contrasena de la boveda se pide en cada ejecucion). \`gitpagedocs chat\` lleva el mismo asistente a la terminal.
|
|
@@ -189,7 +189,7 @@ La configuracion de runtime esta en \`gitpagedocs/config.json\`. Los textos de l
|
|
|
189
189
|
|
|
190
190
|
- \`site.languages\` en \`gitpagedocs/config.json\` activa (\`true\`) o desactiva (\`false\`) cada idioma, en el orden del menu: \`{ "languages": { "en": true, "pt": true, "es": false } }\`. Un idioma en \`false\` desaparece del selector de idiomas y sus textos no se cargan, aunque existan sus docs.
|
|
191
191
|
- \`gitpagedocs/langs/<lang>.json\` guarda los textos de ese idioma: \`langmenu\` (cabecera, busqueda, visor de codigo, reproductor de audio, chat de IA y acceso a los docs) y \`translations\` (\`notFound\`, \`navigation\`, \`footer\`).
|
|
192
|
-
- Para agregar un idioma, crea \`langs/<lang>.json\` y agrega \`"<lang>": true\` en \`site.languages\`.
|
|
192
|
+
- Para agregar un idioma, crea \`langs/<lang>.json\` y agrega \`"<lang>": true\` en \`site.languages\`.
|
|
193
193
|
- Los \`config.json\` antiguos que aun traen \`site.langmenu\` / \`translations\` inline siguen funcionando; cuando existen ambos, los archivos de \`langs/\` prevalecen y cualquier clave ausente se completa desde la version actual.
|
|
194
194
|
|
|
195
195
|
## Seccion \`site\`
|
|
@@ -205,6 +205,7 @@ Claves principales:
|
|
|
205
205
|
- \`docsVersion\`: version seleccionada por defecto
|
|
206
206
|
- \`ActiveNavigation\`: habilita anterior/siguiente
|
|
207
207
|
- \`FocusMode\`: habilita modo foco/lectura
|
|
208
|
+
- \`icons\`: iconos de la cabecera. \`icons.defaults\` guarda lo que todos comparten (\`reactIcon\`, \`colorDark\`, \`colorLight\`, \`size\`, \`imgDark\`, \`imgLight\`, \`imgWidth\`, \`imgHeight\`) y cada icono define su \`tag\` y lo que cambia (\`null\` quita un campo). Las claves planas de abajo siguen valiendo y ganan cuando existen ambas formas.
|
|
208
209
|
- \`IconImageMenuHeaderImgWidth\`, \`IconImageMenuHeaderImgHeight\`: tamano del icono principal
|
|
209
210
|
- \`IconImageMenuHeaderLightImg\`, \`IconImageMenuHeaderDarkImg\`: icono principal (light/dark)
|
|
210
211
|
- \`IconProjectLinkImgWidth\`, \`IconProjectLinkImgHeight\`: tamano del icono enlace del proyecto
|
|
@@ -256,6 +257,8 @@ Los configs de version soportan multiples tipos:
|
|
|
256
257
|
|
|
257
258
|
Cada ruta puede incluir \`title\`, \`description\` (por idioma), \`titleCss\`, \`titlePosition: "center"\`, \`descriptionPosition: "center"\`, \`titleIsVisible\`, \`descriptionIsVisible\`.
|
|
258
259
|
|
|
260
|
+
Los ajustes que comparten todas las rutas se escriben una sola vez en \`routeDefaults\`, al inicio del config de la version; cada ruta guarda solo sus propios valores, y un valor escrito en la ruta gana sobre \`routeDefaults\`.
|
|
261
|
+
|
|
259
262
|
## Variables por ruta (blockLink, container, url, browseAll)
|
|
260
263
|
|
|
261
264
|
Opciones por ruta en \`routes-md\`, \`routes-html\` y \`routes-video\`:
|
package/content/docs-pt.mjs
CHANGED
|
@@ -172,7 +172,7 @@ Os docs trazem um assistente de IA em duas superficies: um **chat drawer** dentr
|
|
|
172
172
|
|
|
173
173
|
- **14 provedores** em um unico core compartilhado: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity.
|
|
174
174
|
- **Escolha de modelo** — a partir do catalogo de cada provedor (\`gitpagedocs models <provedor>\`); um id de modelo salvo que o provedor aposentou e trocado pelo padrao do provedor automaticamente.
|
|
175
|
-
- **Criptografia em repouso** — sua chave de API e selada com AES-256-GCM atras de uma **senha local** e nunca fica em texto puro nem em logs.
|
|
175
|
+
- **Criptografia em repouso** — sua chave de API e selada com AES-256-GCM atras de uma **senha local** e nunca fica em texto puro nem em logs.
|
|
176
176
|
- **Bloqueio por inatividade** — o chat drawer se bloqueia sozinho apos \`site.AiChatAutoLockSeconds\` segundos sem uso (padrao 30, \`0\` desativa): um modal centralizado com contagem regressiva, no seu idioma, permite cancelar ou bloquear agora, e desbloquear pede a senha de novo.
|
|
177
177
|
- **Provedores resilientes** — erros transitorios do provedor sao repetidos (3 tentativas) e uma falha final vira uma mensagem simples terminando em "Tente novamente!".
|
|
178
178
|
- **Geracao de documentacao com IA** — \`gitpagedocs ai\` varre os caminhos escolhidos e escreve markdown multilingue (pt/en/es); reutilizavel via \`.gitpagedocsconfig\`, cuja chave de API fica selada no cofre criptografado \`.gitpagedocsvault\` (a senha do cofre e pedida em toda execucao). \`gitpagedocs chat\` leva o mesmo assistente ao terminal.
|
|
@@ -189,7 +189,7 @@ A configuracao de runtime fica em \`gitpagedocs/config.json\`. Os textos da UI f
|
|
|
189
189
|
|
|
190
190
|
- \`site.languages\` em \`gitpagedocs/config.json\` liga (\`true\`) ou desliga (\`false\`) cada idioma, na ordem do menu: \`{ "languages": { "en": true, "pt": true, "es": false } }\`. Um idioma em \`false\` some do seletor de idiomas e seus textos nao sao carregados, mesmo que os docs existam.
|
|
191
191
|
- \`gitpagedocs/langs/<lang>.json\` guarda os textos daquele idioma: \`langmenu\` (cabecalho, busca, visualizador de codigo, player de audio, chat de IA e acesso aos docs) e \`translations\` (\`notFound\`, \`navigation\`, \`footer\`).
|
|
192
|
-
- Para adicionar um idioma, crie \`langs/<lang>.json\` e inclua \`"<lang>": true\` em \`site.languages\`.
|
|
192
|
+
- Para adicionar um idioma, crie \`langs/<lang>.json\` e inclua \`"<lang>": true\` em \`site.languages\`.
|
|
193
193
|
- Arquivos \`config.json\` antigos que ainda trazem \`site.langmenu\` / \`translations\` inline continuam funcionando; quando os dois existem, os arquivos de \`langs/\` prevalecem e qualquer chave ausente e preenchida a partir da versao atual.
|
|
194
194
|
|
|
195
195
|
## Secao \`site\`
|
|
@@ -205,6 +205,7 @@ Principais chaves:
|
|
|
205
205
|
- \`docsVersion\`: versao inicial selecionada
|
|
206
206
|
- \`ActiveNavigation\`: habilita comportamento de anterior/proximo
|
|
207
207
|
- \`FocusMode\`: habilita modo foco/leitura
|
|
208
|
+
- \`icons\`: icones do cabecalho. \`icons.defaults\` guarda o que todos compartilham (\`reactIcon\`, \`colorDark\`, \`colorLight\`, \`size\`, \`imgDark\`, \`imgLight\`, \`imgWidth\`, \`imgHeight\`) e cada icone define seu \`tag\` e o que muda (\`null\` remove um campo). As chaves planas abaixo continuam valendo e vencem quando as duas formas existem.
|
|
208
209
|
- \`IconImageMenuHeaderImgWidth\`, \`IconImageMenuHeaderImgHeight\`: tamanho do icone principal
|
|
209
210
|
- \`IconImageMenuHeaderLightImg\`, \`IconImageMenuHeaderDarkImg\`: icone principal (light/dark)
|
|
210
211
|
- \`IconProjectLinkImgWidth\`, \`IconProjectLinkImgHeight\`: tamanho do icone link do projeto
|
|
@@ -256,6 +257,8 @@ Configs de versao suportam multiplos tipos:
|
|
|
256
257
|
|
|
257
258
|
Cada rota pode incluir \`title\`, \`description\` (por idioma), \`titleCss\`, \`titlePosition: "center"\`, \`descriptionPosition: "center"\`, \`titleIsVisible\`, \`descriptionIsVisible\`.
|
|
258
259
|
|
|
260
|
+
Configuracoes que todas as rotas compartilham ficam uma unica vez em \`routeDefaults\`, no topo do config da versao; cada rota guarda so os proprios valores, e um valor escrito na rota vence o \`routeDefaults\`.
|
|
261
|
+
|
|
259
262
|
## Variaveis por rota (blockLink, container, url, browseAll)
|
|
260
263
|
|
|
261
264
|
Opcoes por rota em \`routes-md\`, \`routes-html\` e \`routes-video\`:
|
package/contracts/index.mjs
CHANGED
|
@@ -4,24 +4,21 @@
|
|
|
4
4
|
* <outputDir>/config.json `site.languages` — enable (true) / disable (false) each language
|
|
5
5
|
* <outputDir>/langs/<lang>.json UI strings (langmenu + translations) of one language
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @gitpagedocs/tools so the generator and the viewer never disagree about where
|
|
10
|
-
* the bundles live.
|
|
7
|
+
* The file names come from @gitpagedocs/tools so the generator and the viewer
|
|
8
|
+
* never disagree about where the bundles live.
|
|
11
9
|
*/
|
|
12
|
-
import { LANGS_DIRNAME
|
|
10
|
+
import { LANGS_DIRNAME } from "@gitpagedocs/tools/i18n";
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* Repo-relative POSIX paths of the language artifacts for a docs output dir.
|
|
16
14
|
*
|
|
17
15
|
* @param {string} outputDir Docs output dir (e.g. `gitpagedocs`).
|
|
18
|
-
* @returns {{ config: string,
|
|
16
|
+
* @returns {{ config: string, dir: string, bundle: (language: string) => string }}
|
|
19
17
|
*/
|
|
20
18
|
export function languageArtifactPaths(outputDir) {
|
|
21
19
|
const dir = `${outputDir}/${LANGS_DIRNAME}`;
|
|
22
20
|
return {
|
|
23
21
|
config: `${outputDir}/config.json`,
|
|
24
|
-
legacyManifest: `${outputDir}/${LANGS_MANIFEST_FILENAME}`,
|
|
25
22
|
dir,
|
|
26
23
|
bundle: (language) => `${dir}/${language}.json`,
|
|
27
24
|
};
|
package/contracts/languages.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Languages the generator ships UI strings and docs for, in menu order
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Languages the generator ships UI strings and docs for, in menu order (the
|
|
3
|
+
* keys of `site.languages`); every entry must have a `langs/<lang>.json`
|
|
4
|
+
* bundle and a docs content module.
|
|
5
5
|
*/
|
|
6
6
|
export const SUPPORTED_LANGUAGES = Object.freeze(["en", "pt", "es"]);
|
|
@@ -10,8 +10,6 @@ export declare const DEFAULT_LAYOUTS_DIR: string;
|
|
|
10
10
|
export declare const LAYOUTS_CONFIG_FILENAME: string;
|
|
11
11
|
export declare const LAYOUTS_FALLBACK_CONFIG_FILENAME: string;
|
|
12
12
|
export declare const LAYOUTS_TEMPLATES_DIRNAME: string;
|
|
13
|
-
export declare const LEGACY_LAYOUTS_SUBDIR: string;
|
|
14
13
|
|
|
15
14
|
export declare function normalizeLayoutsDir(dir: string): string;
|
|
16
|
-
export declare function legacyLayoutsDir(outputDir: string): string;
|
|
17
15
|
export declare function layoutsArtifactPaths(layoutsDir: string): LayoutsArtifactPaths;
|
|
@@ -4,9 +4,7 @@ import { trimSlashes } from "./path-segments.mjs";
|
|
|
4
4
|
* Canonical locations for layout artifacts.
|
|
5
5
|
*
|
|
6
6
|
* Local layouts are generated into the standalone `gitpagelayouts/` home at the
|
|
7
|
-
* project root.
|
|
8
|
-
* versions wrote: the viewer still reads it, and the CLI offers to migrate it,
|
|
9
|
-
* but nothing is generated there any more.
|
|
7
|
+
* project root.
|
|
10
8
|
*
|
|
11
9
|
* Every consumer derives its paths from here so the folder name exists in one
|
|
12
10
|
* place only.
|
|
@@ -24,9 +22,6 @@ export const LAYOUTS_FALLBACK_CONFIG_FILENAME = "layoutsFallbackConfig.json";
|
|
|
24
22
|
/** Folder holding one JSON template per layout, inside a layouts folder. */
|
|
25
23
|
export const LAYOUTS_TEMPLATES_DIRNAME = "templates";
|
|
26
24
|
|
|
27
|
-
/** Sub-folder of the docs output dir that older versions generated into. */
|
|
28
|
-
export const LEGACY_LAYOUTS_SUBDIR = "layouts";
|
|
29
|
-
|
|
30
25
|
/**
|
|
31
26
|
* Normalize a user-supplied folder name to a repo-relative POSIX path with no
|
|
32
27
|
* leading/trailing separators, so it can be joined and stored in config.json
|
|
@@ -41,16 +36,6 @@ export function normalizeLayoutsDir(dir) {
|
|
|
41
36
|
return normalized || DEFAULT_LAYOUTS_DIR;
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
/**
|
|
45
|
-
* Legacy layouts folder for a given docs output dir.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} outputDir Docs output dir (e.g. `gitpagedocs`).
|
|
48
|
-
* @returns {string} Repo-relative POSIX path (e.g. `gitpagedocs/layouts`).
|
|
49
|
-
*/
|
|
50
|
-
export function legacyLayoutsDir(outputDir) {
|
|
51
|
-
return `${normalizeLayoutsDir(outputDir)}/${LEGACY_LAYOUTS_SUBDIR}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
39
|
/**
|
|
55
40
|
* Repo-relative paths of every artifact a layouts folder owns.
|
|
56
41
|
*
|
package/data/i18n-langmenu.mjs
CHANGED
|
@@ -7,6 +7,10 @@ export const defaultLangMenu = {
|
|
|
7
7
|
menuOpen: "Menu",
|
|
8
8
|
menuClose: "Fechar",
|
|
9
9
|
showMenu: "Abrir menu",
|
|
10
|
+
mdCopyLabel: "Copiar Markdown",
|
|
11
|
+
mdCopiedLabel: "Copiado!",
|
|
12
|
+
mdCopyErrorLabel: "Não foi possível copiar",
|
|
13
|
+
mdDownloadLabel: "Baixar .md",
|
|
10
14
|
hideMenu: "Fechar menu",
|
|
11
15
|
quickNavigation: "Ctrl+K",
|
|
12
16
|
searchOwnerLabel: "Usuario (ex: Vidigal-code)",
|
|
@@ -86,12 +90,24 @@ export const defaultLangMenu = {
|
|
|
86
90
|
aiChatAutoLockDesc: "Você não usou a IA por um tempo. O chat será bloqueado e suas chaves continuarão criptografadas em {seconds}s.",
|
|
87
91
|
aiChatAutoLockConfirmBtn: "Salvar",
|
|
88
92
|
aiChatAutoLockCancelBtn: "Cancelar",
|
|
93
|
+
aiChatInfoBtn: "Como usar e riscos",
|
|
94
|
+
aiChatInfoTitle: "Como funciona o Assistente de IA",
|
|
95
|
+
aiChatInfoIntro: "O assistente responde perguntas sobre esta documentação usando o provedor de IA que você escolher (OpenAI, Anthropic Claude, Google Gemini ou um Ollama local). Este site não tem servidor próprio: o seu navegador fala direto com o provedor usando a sua própria chave de API. Leia os riscos antes de usar.",
|
|
96
|
+
aiChatInfoWorksTitle: "Como funciona",
|
|
97
|
+
aiChatInfoWorksItems: "Senha local: na primeira vez você cria uma senha. Ela nunca sai deste navegador e nunca é gravada; serve só para derivar a chave de criptografia (PBKDF2-SHA-256, 210.000 iterações).\nCofre criptografado: suas chaves de API ficam salvas criptografadas com AES-256-GCM no localStorage deste navegador. Sem a senha, os dados guardados são ilegíveis.\nDesbloqueio: enquanto o chat está desbloqueado, a senha fica só na memória desta página. A chave de API em si não fica guardada descriptografada.\nCada pergunta: a chave é descriptografada no momento do envio, usada só naquela requisição e liberada quando a resposta termina.\nRequisição: a pergunta, a conversa recente e o texto da página que você está lendo vão por HTTPS direto para o provedor escolhido, que devolve a resposta em tempo real.\nBloqueio: o botão de cadeado, ou o tempo de inatividade, remove a senha da página e volta para a tela de senha. As chaves salvas continuam criptografadas.",
|
|
98
|
+
aiChatInfoHowTitle: "Como usar",
|
|
99
|
+
aiChatInfoHowItems: "Crie uma senha local (mínimo de 4 caracteres; uma senha longa e única é mais segura). Anote: ela não pode ser recuperada.\nEscolha o provedor e o modelo e cole a sua chave de API. No Ollama, informe o endereço do servidor local (por exemplo http://localhost:11434).\nPergunte sobre a página que está lendo; a resposta aparece em tempo real. Você pode anexar imagens nos provedores que aceitam.\nAo terminar, clique no cadeado. Para voltar, digite a senha de novo.\nBotões do topo: engrenagem troca provedor ou chave; balão limpa a conversa; banco de dados apaga as chaves salvas; ponto de exclamação abre este guia.",
|
|
100
|
+
aiChatInfoFlowTitle: "O que pode acontecer",
|
|
101
|
+
aiChatInfoFlowItems: "Senha errada: o cofre não abre e nada é apagado. Você pode tentar de novo quantas vezes quiser.\nEsqueceu a senha: \"Redefinir senha\" apaga todas as chaves salvas e a senha; depois você cria uma senha nova e cola as chaves de novo.\nInatividade: depois de {seconds}s sem usar o chat aparece uma contagem regressiva. Cancelar mantém a sessão; senão o chat é bloqueado e pede a senha.\nErro temporário do provedor (limite de uso, servidor ocupado ou fora do ar): a requisição é repetida automaticamente até 3 vezes. Se continuar falhando, aparece uma mensagem terminando em \"Tente novamente!\".\nChave inválida, vencida ou sem crédito: o provedor recusa a requisição e aparece um erro de autenticação. Atualize a chave pela engrenagem.\nModelo desativado pelo provedor: o chat troca sozinho para o modelo padrão daquele provedor.\nOllama: precisa estar rodando no seu computador e aceitar requisições deste site (CORS). Caso contrário aparece erro de conexão.\nSem internet ou rede que bloqueia o provedor: a requisição falha com erro de conexão; nada é perdido.\nLimpar os dados do navegador, usar janela anônima ou outro navegador: o cofre não existe lá, então você cria uma senha e cola a chave de novo.",
|
|
102
|
+
aiChatInfoRisksTitle: "Riscos que você corre",
|
|
103
|
+
aiChatInfoRisksItems: "A chave viaja em cada requisição. Quem usar este computador com as ferramentas de desenvolvedor abertas (aba Rede) consegue vê-la enquanto você conversa. No Google Gemini ela vai na URL da requisição.\nUma senha fraca pode ser descoberta por quem copiar o armazenamento deste navegador. Use uma senha longa que você não usa em nenhum outro lugar.\nO bloqueio remove a senha da página, mas o navegador só libera essa memória depois. Não use o assistente em computadores compartilhados ou públicos.\nExtensões do navegador com acesso a este site podem ler o que a página vê, inclusive a chave enquanto está descriptografada. Use um perfil de navegador sem extensões desconhecidas.\nSuas perguntas, a conversa e o texto da página atual são enviados ao provedor escolhido e seguem a política de privacidade dele. Não envie senhas, dados pessoais ou informações confidenciais.\nO uso é cobrado na sua conta do provedor. Configure limites de gasto no painel do provedor e revogue a chave na hora se suspeitar de vazamento.\nRespostas de IA podem estar erradas ou inventadas. Confira informações importantes na própria documentação.",
|
|
104
|
+
aiChatInfoBackBtn: "Voltar ao chat",
|
|
89
105
|
aiChatProviderLabel: "Provedor:",
|
|
90
106
|
aiChatApiKeyLabel: "Chave de API (deixe em branco para IA local):",
|
|
91
|
-
aiChatProviderOpenAI: "OpenAI
|
|
92
|
-
aiChatProviderClaude: "Anthropic Claude
|
|
93
|
-
aiChatProviderGemini: "Google Gemini
|
|
94
|
-
aiChatProviderOllama: "Ollama
|
|
107
|
+
aiChatProviderOpenAI: "OpenAI",
|
|
108
|
+
aiChatProviderClaude: "Anthropic Claude",
|
|
109
|
+
aiChatProviderGemini: "Google Gemini",
|
|
110
|
+
aiChatProviderOllama: "Ollama",
|
|
95
111
|
aiChatEmptyPageContent: "Nenhuma página ativa compatível.",
|
|
96
112
|
aiChatNoPageId: "Sem página",
|
|
97
113
|
aiChatOllamaUrlLabel: "URL da API Ollama (deixe em branco para local)",
|
|
@@ -128,6 +144,10 @@ export const defaultLangMenu = {
|
|
|
128
144
|
menuOpen: "Menu",
|
|
129
145
|
menuClose: "Close",
|
|
130
146
|
showMenu: "Show menu",
|
|
147
|
+
mdCopyLabel: "Copy markdown",
|
|
148
|
+
mdCopiedLabel: "Copied!",
|
|
149
|
+
mdCopyErrorLabel: "Could not copy",
|
|
150
|
+
mdDownloadLabel: "Download .md",
|
|
131
151
|
hideMenu: "Hide menu",
|
|
132
152
|
quickNavigation: "Ctrl+K",
|
|
133
153
|
searchOwnerLabel: "Owner (ex: Vidigal-code)",
|
|
@@ -207,12 +227,24 @@ export const defaultLangMenu = {
|
|
|
207
227
|
aiChatAutoLockDesc: "You have not used the AI for a while. The chat locks and your keys stay encrypted in {seconds}s.",
|
|
208
228
|
aiChatAutoLockConfirmBtn: "OK",
|
|
209
229
|
aiChatAutoLockCancelBtn: "Cancel",
|
|
230
|
+
aiChatInfoBtn: "How to use and risks",
|
|
231
|
+
aiChatInfoTitle: "How the AI Assistant works",
|
|
232
|
+
aiChatInfoIntro: "The assistant answers questions about this documentation using an AI provider you choose (OpenAI, Anthropic Claude, Google Gemini or a local Ollama). This site has no server of its own: your browser talks directly to the provider with your own API key. Read the risks before using it.",
|
|
233
|
+
aiChatInfoWorksTitle: "How it works",
|
|
234
|
+
aiChatInfoWorksItems: "Local password: the first time, you create a password. It never leaves this browser and is never stored; it only derives the encryption key (PBKDF2-SHA-256, 210,000 iterations).\nEncrypted vault: your API keys are saved encrypted with AES-256-GCM in this browser's localStorage. Without the password the stored data is unreadable.\nUnlocking: while the chat is unlocked, the password is kept only in this page's memory. The API key itself is not kept decrypted.\nEach question: the key is decrypted at the moment you send, used for that single request and released when the answer ends.\nRequest: the question, the recent conversation and the text of the page you are reading are sent over HTTPS directly to the chosen provider, which streams the answer back.\nLocking: the lock button, or the inactivity timer, removes the password from the page and brings back the password screen. The saved keys stay encrypted.",
|
|
235
|
+
aiChatInfoHowTitle: "How to use",
|
|
236
|
+
aiChatInfoHowItems: "Create a local password (at least 4 characters; a long, unique password is safer). Write it down: it cannot be recovered.\nPick the provider and model, then paste your API key. For Ollama, enter the address of your local server (for example http://localhost:11434).\nAsk about the page you are reading; the answer appears in real time. You can attach images to providers that support them.\nWhen you finish, press the lock button. To come back, type the password again.\nHeader buttons: gear changes provider or key; speech bubble clears the conversation; database erases the saved keys; exclamation mark opens this guide.",
|
|
237
|
+
aiChatInfoFlowTitle: "What can happen",
|
|
238
|
+
aiChatInfoFlowItems: "Wrong password: the vault does not open and nothing is erased. You can try again as many times as you want.\nForgot the password: \"Reset password\" erases every saved key and the password; then you create a new password and paste the keys again.\nInactivity: after {seconds}s without using the chat, a countdown appears. Cancel keeps the session; otherwise the chat locks and asks for the password.\nTemporary provider error (rate limit, busy or unavailable server): the request is retried automatically up to 3 times. If it keeps failing, a message ending in \"Try again!\" appears.\nInvalid, expired or unfunded key: the provider refuses the request and an authentication error appears. Update the key with the gear button.\nModel retired by the provider: the chat switches automatically to that provider's default model.\nOllama: it must be running on your computer and allow requests from this site (CORS). Otherwise a connection error appears.\nNo internet or a blocking network: the request fails with a connection error; nothing is lost.\nClearing browser data, using a private window or another browser: the vault is not there, so you create a password and paste the key again.",
|
|
239
|
+
aiChatInfoRisksTitle: "Risks you take",
|
|
240
|
+
aiChatInfoRisksItems: "The key travels in every request. Anyone using this computer with the developer tools open (Network tab) can see it while you chat. With Google Gemini it goes in the request URL.\nA weak password can be guessed by someone who copies this browser's storage. Use a long password you do not use anywhere else.\nLocking removes the password from the page, but the browser frees that memory only later. Do not use the assistant on shared or public computers.\nBrowser extensions with access to this site can read what the page sees, including the key while it is decrypted. Use a browser profile without unknown extensions.\nYour questions, the conversation and the text of the current page are sent to the chosen provider and fall under its privacy policy. Do not send passwords, personal data or confidential information.\nUsage is billed to your provider account. Set spending limits in the provider's dashboard and revoke the key right away if you suspect a leak.\nAI answers can be wrong or invented. Check important information in the documentation itself.",
|
|
241
|
+
aiChatInfoBackBtn: "Back to chat",
|
|
210
242
|
aiChatProviderLabel: "Provider:",
|
|
211
243
|
aiChatApiKeyLabel: "API Key (leave blank for Local AI):",
|
|
212
|
-
aiChatProviderOpenAI: "OpenAI
|
|
213
|
-
aiChatProviderClaude: "Anthropic Claude
|
|
214
|
-
aiChatProviderGemini: "Google Gemini
|
|
215
|
-
aiChatProviderOllama: "Ollama
|
|
244
|
+
aiChatProviderOpenAI: "OpenAI",
|
|
245
|
+
aiChatProviderClaude: "Anthropic Claude",
|
|
246
|
+
aiChatProviderGemini: "Google Gemini",
|
|
247
|
+
aiChatProviderOllama: "Ollama",
|
|
216
248
|
aiChatEmptyPageContent: "No compatible active page.",
|
|
217
249
|
aiChatNoPageId: "No page",
|
|
218
250
|
aiChatOllamaUrlLabel: "Ollama API URL (leave blank for local)",
|
|
@@ -249,6 +281,10 @@ export const defaultLangMenu = {
|
|
|
249
281
|
menuOpen: "Menu",
|
|
250
282
|
menuClose: "Cerrar",
|
|
251
283
|
showMenu: "Abrir menu",
|
|
284
|
+
mdCopyLabel: "Copiar Markdown",
|
|
285
|
+
mdCopiedLabel: "¡Copiado!",
|
|
286
|
+
mdCopyErrorLabel: "No se pudo copiar",
|
|
287
|
+
mdDownloadLabel: "Descargar .md",
|
|
252
288
|
hideMenu: "Cerrar menu",
|
|
253
289
|
quickNavigation: "Ctrl+K",
|
|
254
290
|
searchOwnerLabel: "Usuario (ej: Vidigal-code)",
|
|
@@ -328,12 +364,24 @@ export const defaultLangMenu = {
|
|
|
328
364
|
aiChatAutoLockDesc: "No has usado la IA por un tiempo. El chat se bloqueará y tus claves seguirán cifradas en {seconds}s.",
|
|
329
365
|
aiChatAutoLockConfirmBtn: "Guardar",
|
|
330
366
|
aiChatAutoLockCancelBtn: "Cancelar",
|
|
367
|
+
aiChatInfoBtn: "Cómo usar y riesgos",
|
|
368
|
+
aiChatInfoTitle: "Cómo funciona el Asistente de IA",
|
|
369
|
+
aiChatInfoIntro: "El asistente responde preguntas sobre esta documentación usando el proveedor de IA que elijas (OpenAI, Anthropic Claude, Google Gemini o un Ollama local). Este sitio no tiene servidor propio: tu navegador habla directamente con el proveedor usando tu propia clave de API. Lee los riesgos antes de usarlo.",
|
|
370
|
+
aiChatInfoWorksTitle: "Cómo funciona",
|
|
371
|
+
aiChatInfoWorksItems: "Contraseña local: la primera vez creas una contraseña. Nunca sale de este navegador y nunca se guarda; solo sirve para derivar la clave de cifrado (PBKDF2-SHA-256, 210.000 iteraciones).\nBóveda cifrada: tus claves de API se guardan cifradas con AES-256-GCM en el localStorage de este navegador. Sin la contraseña, los datos guardados son ilegibles.\nDesbloqueo: mientras el chat está desbloqueado, la contraseña queda solo en la memoria de esta página. La clave de API en sí no se guarda descifrada.\nCada pregunta: la clave se descifra en el momento del envío, se usa solo en esa solicitud y se libera cuando termina la respuesta.\nSolicitud: la pregunta, la conversación reciente y el texto de la página que estás leyendo van por HTTPS directamente al proveedor elegido, que devuelve la respuesta en tiempo real.\nBloqueo: el botón de candado, o el tiempo de inactividad, quita la contraseña de la página y vuelve a la pantalla de contraseña. Las claves guardadas siguen cifradas.",
|
|
372
|
+
aiChatInfoHowTitle: "Cómo usar",
|
|
373
|
+
aiChatInfoHowItems: "Crea una contraseña local (mínimo 4 caracteres; una contraseña larga y única es más segura). Anótala: no se puede recuperar.\nElige el proveedor y el modelo y pega tu clave de API. En Ollama, indica la dirección del servidor local (por ejemplo http://localhost:11434).\nPregunta sobre la página que estás leyendo; la respuesta aparece en tiempo real. Puedes adjuntar imágenes en los proveedores que lo admiten.\nAl terminar, pulsa el candado. Para volver, escribe la contraseña de nuevo.\nBotones superiores: engranaje cambia proveedor o clave; globo limpia la conversación; base de datos borra las claves guardadas; signo de exclamación abre esta guía.",
|
|
374
|
+
aiChatInfoFlowTitle: "Qué puede pasar",
|
|
375
|
+
aiChatInfoFlowItems: "Contraseña incorrecta: la bóveda no se abre y no se borra nada. Puedes intentarlo cuantas veces quieras.\nOlvidaste la contraseña: \"Restablecer contraseña\" borra todas las claves guardadas y la contraseña; luego creas una contraseña nueva y pegas las claves otra vez.\nInactividad: tras {seconds}s sin usar el chat aparece una cuenta regresiva. Cancelar mantiene la sesión; si no, el chat se bloquea y pide la contraseña.\nError temporal del proveedor (límite de uso, servidor ocupado o caído): la solicitud se repite automáticamente hasta 3 veces. Si sigue fallando, aparece un mensaje que termina en \"¡Inténtalo de nuevo!\".\nClave inválida, vencida o sin saldo: el proveedor rechaza la solicitud y aparece un error de autenticación. Actualiza la clave con el engranaje.\nModelo retirado por el proveedor: el chat cambia solo al modelo predeterminado de ese proveedor.\nOllama: debe estar ejecutándose en tu computadora y aceptar solicitudes de este sitio (CORS). Si no, aparece un error de conexión.\nSin internet o con una red que bloquea al proveedor: la solicitud falla con error de conexión; no se pierde nada.\nBorrar los datos del navegador, usar una ventana privada u otro navegador: la bóveda no existe allí, así que creas una contraseña y pegas la clave otra vez.",
|
|
376
|
+
aiChatInfoRisksTitle: "Riesgos que corres",
|
|
377
|
+
aiChatInfoRisksItems: "La clave viaja en cada solicitud. Quien use esta computadora con las herramientas de desarrollador abiertas (pestaña Red) puede verla mientras conversas. En Google Gemini va en la URL de la solicitud.\nUna contraseña débil puede ser descubierta por quien copie el almacenamiento de este navegador. Usa una contraseña larga que no uses en ningún otro lugar.\nEl bloqueo quita la contraseña de la página, pero el navegador libera esa memoria solo después. No uses el asistente en computadoras compartidas o públicas.\nLas extensiones del navegador con acceso a este sitio pueden leer lo que ve la página, incluida la clave mientras está descifrada. Usa un perfil de navegador sin extensiones desconocidas.\nTus preguntas, la conversación y el texto de la página actual se envían al proveedor elegido y quedan sujetos a su política de privacidad. No envíes contraseñas, datos personales ni información confidencial.\nEl uso se cobra en tu cuenta del proveedor. Configura límites de gasto en el panel del proveedor y revoca la clave de inmediato si sospechas una filtración.\nLas respuestas de IA pueden ser incorrectas o inventadas. Verifica la información importante en la propia documentación.",
|
|
378
|
+
aiChatInfoBackBtn: "Volver al chat",
|
|
331
379
|
aiChatProviderLabel: "Proveedor:",
|
|
332
380
|
aiChatApiKeyLabel: "Clave de API (déjalo en blanco para IA local):",
|
|
333
|
-
aiChatProviderOpenAI: "OpenAI
|
|
334
|
-
aiChatProviderClaude: "Anthropic Claude
|
|
335
|
-
aiChatProviderGemini: "Google Gemini
|
|
336
|
-
aiChatProviderOllama: "Ollama
|
|
381
|
+
aiChatProviderOpenAI: "OpenAI",
|
|
382
|
+
aiChatProviderClaude: "Anthropic Claude",
|
|
383
|
+
aiChatProviderGemini: "Google Gemini",
|
|
384
|
+
aiChatProviderOllama: "Ollama",
|
|
337
385
|
aiChatEmptyPageContent: "Ninguna página activa compatible.",
|
|
338
386
|
aiChatNoPageId: "Sin página",
|
|
339
387
|
aiChatOllamaUrlLabel: "URL de la API de Ollama (déjalo en blanco para local)",
|
package/data/urls.mjs
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
/** Official layout URLs
|
|
2
|
-
* The canonical home is the root `gitpagelayouts/` folder; the legacy
|
|
3
|
-
* `gitpagedocs/layouts/` folder stays online for older deployments. */
|
|
1
|
+
/** Official layout URLs: the root `gitpagelayouts/` folder of git-page-docs. */
|
|
4
2
|
|
|
5
3
|
export const OFFICIAL_LAYOUTS_CONFIG_URL =
|
|
6
4
|
"https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagelayouts/layoutsConfig.json";
|
|
7
5
|
|
|
8
6
|
export const OFFICIAL_LAYOUTS_TEMPLATES_URL =
|
|
9
7
|
"https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagelayouts/templates";
|
|
10
|
-
|
|
11
|
-
export const LEGACY_OFFICIAL_LAYOUTS_CONFIG_URL =
|
|
12
|
-
"https://github.com/Vidigal-code/git-page-docs/blob/main/gitpagedocs/layouts/layoutsConfig.json";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitpagedocs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gitpagedocs": "index.mjs"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.5.1",
|
|
32
|
-
"@gitpagedocs/mcp": "^0.0.
|
|
33
|
-
"@gitpagedocs/tools": "^0.0.
|
|
32
|
+
"@gitpagedocs/mcp": "^0.0.4",
|
|
33
|
+
"@gitpagedocs/tools": "^0.0.4",
|
|
34
34
|
"tsx": "^4.21.0"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
@@ -219,9 +219,8 @@ function printSetupHelp(providerHint: string): void {
|
|
|
219
219
|
*/
|
|
220
220
|
export async function runChat(ctx: CommandContext): Promise<void> {
|
|
221
221
|
const flags = parseChatArgs(ctx.args);
|
|
222
|
-
const configRepo = new AiConfigFileRepository(
|
|
222
|
+
const configRepo = new AiConfigFileRepository();
|
|
223
223
|
const creds: ResolvedChatCredentials | null = await resolveChatCredentials({
|
|
224
|
-
cwd: ctx.cwd,
|
|
225
224
|
providerOverride: flags.provider,
|
|
226
225
|
modelOverride: flags.model,
|
|
227
226
|
configRepo,
|
|
@@ -4,11 +4,11 @@ import type { CommandContext } from "./run-command";
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* `gitpagedocs config clear` — delete the stored `.gitpagedocsconfig` from the
|
|
7
|
-
* per-user config directory
|
|
8
|
-
*
|
|
7
|
+
* per-user config directory together with the encrypted key vault, wiping the
|
|
8
|
+
* saved AI credentials, provider and scan paths.
|
|
9
9
|
*/
|
|
10
|
-
export async function runConfigClear(
|
|
11
|
-
const repository = new AiConfigFileRepository(
|
|
10
|
+
export async function runConfigClear(_ctx: CommandContext): Promise<void> {
|
|
11
|
+
const repository = new AiConfigFileRepository();
|
|
12
12
|
const vault = new AiKeyVault();
|
|
13
13
|
const removed = await repository.clear();
|
|
14
14
|
if (await vault.clear()) removed.push(vault.getVaultPath());
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_LANGS_MANIFEST_PATH,
|
|
5
|
-
defaultConfigLoader,
|
|
6
|
-
getEnabledLanguages,
|
|
7
|
-
isAppError,
|
|
8
|
-
parseLanguageManifest,
|
|
9
|
-
parseLanguageToggles,
|
|
10
|
-
} from "@gitpagedocs/tools";
|
|
2
|
+
import { defaultConfigLoader, getEnabledLanguages, isAppError, parseLanguageToggles } from "@gitpagedocs/tools";
|
|
11
3
|
import type { CommandContext } from "./run-command";
|
|
12
4
|
|
|
13
5
|
const UNKNOWN = "—";
|
|
@@ -17,19 +9,8 @@ interface LanguageSummary {
|
|
|
17
9
|
disabled: string[];
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const manifest = parseLanguageManifest(JSON.parse(await readFile(path.join(cwd, DEFAULT_LANGS_MANIFEST_PATH), "utf-8")));
|
|
24
|
-
if (manifest) return manifest.languages;
|
|
25
|
-
} catch {
|
|
26
|
-
// No manifest: nothing to report.
|
|
27
|
-
}
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** `site.languages` toggles from config.json, else the legacy inline list, else the legacy manifest. */
|
|
32
|
-
async function summarizeLanguages(cwd: string, site: Record<string, unknown>): Promise<LanguageSummary> {
|
|
12
|
+
/** `site.languages` toggles from config.json, else the resolved `supportedLanguages` list. */
|
|
13
|
+
function summarizeLanguages(site: Record<string, unknown>): LanguageSummary {
|
|
33
14
|
const toggles = parseLanguageToggles(site.languages);
|
|
34
15
|
if (toggles) {
|
|
35
16
|
const enabled = getEnabledLanguages(toggles);
|
|
@@ -38,7 +19,7 @@ async function summarizeLanguages(cwd: string, site: Record<string, unknown>): P
|
|
|
38
19
|
if (Array.isArray(site.supportedLanguages)) {
|
|
39
20
|
return { enabled: site.supportedLanguages as string[], disabled: [] };
|
|
40
21
|
}
|
|
41
|
-
return { enabled:
|
|
22
|
+
return { enabled: [], disabled: [] };
|
|
42
23
|
}
|
|
43
24
|
|
|
44
25
|
function formatList(languages: string[]): string {
|
|
@@ -55,7 +36,7 @@ export async function runConfig(ctx: CommandContext): Promise<void> {
|
|
|
55
36
|
try {
|
|
56
37
|
const { config, sourcePath, extension } = await defaultConfigLoader.loadGitPageDocsConfig(ctx.cwd);
|
|
57
38
|
const site = (config as { site?: Record<string, unknown> }).site ?? {};
|
|
58
|
-
const languages =
|
|
39
|
+
const languages = summarizeLanguages(site);
|
|
59
40
|
// eslint-disable-next-line no-console
|
|
60
41
|
console.log(
|
|
61
42
|
`\n Config: ${path.relative(ctx.cwd, sourcePath)} (${extension})\n` +
|
package/presentation/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { printBanner, printCredits } from "./ui/banner";
|
|
6
6
|
import { resolveOptions } from "./options/resolver";
|
|
7
|
-
import { migrateLegacyLayoutsInteractive } from "./ui/layouts-prompts";
|
|
8
7
|
import { runNewCommand } from "./commands/run-command";
|
|
9
8
|
import { dispatchMode } from "../application/use-cases/dispatch-mode";
|
|
10
9
|
import type {
|
|
@@ -60,12 +59,6 @@ async function main(): Promise<void> {
|
|
|
60
59
|
|
|
61
60
|
const options = await resolveOptions(process.argv, process.env);
|
|
62
61
|
|
|
63
|
-
// Layouts moved out of <outputDir>/layouts/. Offer to relocate anything left
|
|
64
|
-
// behind before generating, so the config written next points at one folder.
|
|
65
|
-
if (options.mode === "config-only") {
|
|
66
|
-
await migrateLegacyLayoutsInteractive(root, options);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
62
|
const configOnlyRuntime = createConfigOnlyRuntime() as ConfigOnlyRuntimePort;
|
|
70
63
|
const homeRuntime = createHomeRuntime() as HomeRuntimePort;
|
|
71
64
|
await dispatchMode(options, params, {
|
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { askConfirm, askSelect, askText, note } from "./clack";
|
|
7
|
-
import { interactivePromptsAvailable } from "./tty";
|
|
8
|
-
import {
|
|
9
|
-
DEFAULT_LAYOUTS_DIR,
|
|
10
|
-
normalizeLayoutsDir,
|
|
11
|
-
legacyLayoutsDir,
|
|
12
|
-
} from "../../contracts/layouts-paths.mjs";
|
|
13
|
-
import { planLayoutsMigration } from "../../domain/services/layouts-migration.mjs";
|
|
14
|
-
import { listFilesRecursively, executeLayoutsMigration } from "../../runtime/layouts-migrator.mjs";
|
|
1
|
+
/** Layout-related prompts: where layouts come from and where they are written. */
|
|
2
|
+
import { askSelect, askText } from "./clack";
|
|
3
|
+
import { DEFAULT_LAYOUTS_DIR, normalizeLayoutsDir } from "../../contracts/layouts-paths.mjs";
|
|
15
4
|
|
|
16
5
|
type LayoutsSource = "official" | "local";
|
|
17
6
|
|
|
18
|
-
/** Outcome of a completed migration, for the caller to report. */
|
|
19
|
-
export interface LayoutsMigrationOutcome {
|
|
20
|
-
from: string;
|
|
21
|
-
to: string;
|
|
22
|
-
movedCount: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
7
|
/** Ask whether layouts come from the official remote source or a local folder. */
|
|
26
8
|
export async function askLayoutsSource(useLocal: boolean): Promise<boolean> {
|
|
27
9
|
const choice = await askSelect<LayoutsSource>(
|
|
@@ -52,73 +34,3 @@ export async function askLayoutsDir(current: string): Promise<string> {
|
|
|
52
34
|
});
|
|
53
35
|
return normalizeLayoutsDir(answer);
|
|
54
36
|
}
|
|
55
|
-
|
|
56
|
-
function toAbsolute(root: string, repoRelative: string): string {
|
|
57
|
-
return path.join(root, ...repoRelative.split("/"));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Offer to move layouts left behind in the legacy folder.
|
|
62
|
-
*
|
|
63
|
-
* Nothing is moved without an explicit confirmation. Outside a TTY the legacy
|
|
64
|
-
* folder is reported and left alone — the viewer still reads it, so declining
|
|
65
|
-
* (or automating) never breaks an existing site.
|
|
66
|
-
*
|
|
67
|
-
* @returns The outcome when files were moved, otherwise `null`.
|
|
68
|
-
*/
|
|
69
|
-
export async function migrateLegacyLayoutsInteractive(
|
|
70
|
-
root: string,
|
|
71
|
-
options: { outputDir: string; layoutsDir: string },
|
|
72
|
-
): Promise<LayoutsMigrationOutcome | null> {
|
|
73
|
-
const from = legacyLayoutsDir(options.outputDir);
|
|
74
|
-
const to = normalizeLayoutsDir(options.layoutsDir);
|
|
75
|
-
|
|
76
|
-
const plan = planLayoutsMigration({
|
|
77
|
-
outputDir: options.outputDir,
|
|
78
|
-
layoutsDir: to,
|
|
79
|
-
legacyFiles: listFilesRecursively(toAbsolute(root, from)),
|
|
80
|
-
targetFiles: listFilesRecursively(toAbsolute(root, to)),
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (!plan.required) return null;
|
|
84
|
-
|
|
85
|
-
if (!interactivePromptsAvailable()) {
|
|
86
|
-
note(
|
|
87
|
-
`${plan.files.length} layout file(s) still live in ${plan.from}/.\n` +
|
|
88
|
-
`The viewer still reads them. Run this in a terminal to move them to ${plan.to}/.`,
|
|
89
|
-
"Legacy layouts",
|
|
90
|
-
);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const overwriteWarning =
|
|
95
|
-
plan.overwrites.length > 0
|
|
96
|
-
? `\n${plan.overwrites.length} file(s) already in ${plan.to}/ would be replaced.`
|
|
97
|
-
: "";
|
|
98
|
-
note(
|
|
99
|
-
`Found ${plan.files.length} layout file(s) in ${plan.from}/.\n` +
|
|
100
|
-
`Layouts are now generated in ${plan.to}/.${overwriteWarning}`,
|
|
101
|
-
"Legacy layouts",
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const confirmed = await askConfirm(
|
|
105
|
-
`Move them to ${plan.to}/ now?`,
|
|
106
|
-
plan.overwrites.length === 0,
|
|
107
|
-
);
|
|
108
|
-
if (!confirmed) {
|
|
109
|
-
note(`Left untouched — the viewer still reads ${plan.from}/.`, "Legacy layouts");
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const result = executeLayoutsMigration(root, plan);
|
|
114
|
-
|
|
115
|
-
if (result.failed.length > 0) {
|
|
116
|
-
note(
|
|
117
|
-
`${result.failed.length} file(s) could not be moved:\n` +
|
|
118
|
-
result.failed.map((entry) => `- ${entry.file}: ${entry.reason}`).join("\n"),
|
|
119
|
-
"Migration incomplete",
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { from: plan.from, to: plan.to, movedCount: result.moved.length };
|
|
124
|
-
}
|
package/runtime/output.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { layoutsArtifactPaths } from "../contracts/layouts-paths.mjs";
|
|
12
12
|
import { languageArtifactPaths } from "../contracts/langs-paths.mjs";
|
|
13
13
|
import { SUPPORTED_LANGUAGES } from "../contracts/languages.mjs";
|
|
14
|
+
import { compactSiteIcons, factorRouteDefaults } from "@gitpagedocs/tools/config-format";
|
|
14
15
|
|
|
15
16
|
const DEFAULT_ICON_SVG = `<?xml version="1.0" encoding="utf-8"?>
|
|
16
17
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -20,28 +21,6 @@ const DEFAULT_ICON_SVG = `<?xml version="1.0" encoding="utf-8"?>
|
|
|
20
21
|
</svg>
|
|
21
22
|
`;
|
|
22
23
|
|
|
23
|
-
function listExistingVersionIds(root, outputDir) {
|
|
24
|
-
const versionsRoot = path.join(root, outputDir, "docs", "versions");
|
|
25
|
-
if (!existsSync(versionsRoot)) return [];
|
|
26
|
-
return readdirSync(versionsRoot, { withFileTypes: true })
|
|
27
|
-
.filter((entry) => entry.isDirectory())
|
|
28
|
-
.map((entry) => entry.name);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function removeLegacySourceViewerFiles(root, outputDir) {
|
|
32
|
-
const versionIds = listExistingVersionIds(root, outputDir);
|
|
33
|
-
for (const versionId of versionIds) {
|
|
34
|
-
for (const language of SUPPORTED_LANGUAGES) {
|
|
35
|
-
for (const fileName of ["source-viewer", "source-viewer.html"]) {
|
|
36
|
-
const legacyPath = path.join(root, outputDir, "docs", "versions", versionId, language, fileName);
|
|
37
|
-
if (existsSync(legacyPath)) {
|
|
38
|
-
rmSync(legacyPath, { force: true });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
24
|
async function writeJson(root, relativePath, data) {
|
|
46
25
|
const absolutePath = path.join(root, relativePath);
|
|
47
26
|
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
@@ -77,32 +56,17 @@ async function writeLayoutArtifacts(options) {
|
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
58
|
|
|
80
|
-
/**
|
|
81
|
-
* Write one UI-strings bundle per shipped language. Which ones are on is
|
|
82
|
-
* `site.languages` in config.json, so the 1.1.68 `langs.json` manifest is
|
|
83
|
-
* removed: a stale copy would only mislead.
|
|
84
|
-
*/
|
|
59
|
+
/** Write one UI-strings bundle per shipped language; `site.languages` in config.json switches them. */
|
|
85
60
|
async function writeLanguageArtifacts(root, outputDir, artifacts) {
|
|
86
61
|
const paths = languageArtifactPaths(outputDir);
|
|
87
|
-
rmSync(path.join(root, paths.legacyManifest), { force: true });
|
|
88
62
|
for (const [language, bundle] of Object.entries(artifacts.languageBundles)) {
|
|
89
63
|
await writeJson(root, paths.bundle(language), bundle);
|
|
90
64
|
}
|
|
91
65
|
}
|
|
92
66
|
|
|
93
|
-
/** Keep only versioned docs output in docs/, removing legacy root language folders. */
|
|
94
|
-
function removeLegacyLanguageDirs(root, outputDir) {
|
|
95
|
-
for (const legacyLanguageDir of SUPPORTED_LANGUAGES) {
|
|
96
|
-
const legacyPath = path.join(root, outputDir, "docs", legacyLanguageDir);
|
|
97
|
-
if (existsSync(legacyPath)) {
|
|
98
|
-
rmSync(legacyPath, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
67
|
async function writeVersionConfigs(root, outputDir, versionConfigs) {
|
|
104
68
|
for (const [versionId, versionConfig] of Object.entries(versionConfigs)) {
|
|
105
|
-
await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, versionConfig);
|
|
69
|
+
await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, factorRouteDefaults(versionConfig));
|
|
106
70
|
}
|
|
107
71
|
}
|
|
108
72
|
|
|
@@ -180,9 +144,8 @@ export async function writeConfigOnlyOutput(options) {
|
|
|
180
144
|
createThemeTemplate,
|
|
181
145
|
} = options;
|
|
182
146
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
await writeJson(root, `${outputDir}/config.json`, artifacts.rootConfig);
|
|
147
|
+
// Stored compact: shared icon values and route settings are written once.
|
|
148
|
+
await writeJson(root, `${outputDir}/config.json`, { ...artifacts.rootConfig, site: compactSiteIcons(artifacts.rootConfig.site) });
|
|
186
149
|
await writeLanguageArtifacts(root, outputDir, artifacts);
|
|
187
150
|
await writeText(root, `${outputDir}/icon.svg`, DEFAULT_ICON_SVG);
|
|
188
151
|
await writeLayoutArtifacts({
|
|
@@ -195,7 +158,6 @@ export async function writeConfigOnlyOutput(options) {
|
|
|
195
158
|
});
|
|
196
159
|
await writeVersionConfigs(root, outputDir, artifacts.versionConfigs);
|
|
197
160
|
|
|
198
|
-
removeLegacySourceViewerFiles(root, outputDir);
|
|
199
161
|
|
|
200
162
|
await writeVersionedDocs(root, outputDir, artifacts);
|
|
201
163
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/** A planned move from the legacy layouts folder to the standalone home. */
|
|
2
|
-
export interface LayoutsMigrationPlan {
|
|
3
|
-
required: boolean;
|
|
4
|
-
from: string;
|
|
5
|
-
to: string;
|
|
6
|
-
files: string[];
|
|
7
|
-
overwrites: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export declare function planLayoutsMigration(options: {
|
|
11
|
-
outputDir: string;
|
|
12
|
-
layoutsDir: string;
|
|
13
|
-
legacyFiles?: string[];
|
|
14
|
-
targetFiles?: string[];
|
|
15
|
-
}): LayoutsMigrationPlan;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legacy layouts migration — planning only.
|
|
3
|
-
*
|
|
4
|
-
* Deciding *what* should move is domain logic, so it stays free of prompts and
|
|
5
|
-
* filesystem writes: callers list the two folders, ask the user, and perform
|
|
6
|
-
* the move. That keeps the rule testable with plain arrays.
|
|
7
|
-
*/
|
|
8
|
-
import { legacyLayoutsDir, normalizeLayoutsDir } from "../../contracts/layouts-paths.mjs";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {Object} LayoutsMigrationPlan
|
|
12
|
-
* @property {boolean} required True when the legacy folder holds files to move.
|
|
13
|
-
* @property {string} from Repo-relative legacy folder.
|
|
14
|
-
* @property {string} to Repo-relative destination folder.
|
|
15
|
-
* @property {string[]} files Paths relative to `from`, sorted.
|
|
16
|
-
* @property {string[]} overwrites Subset of `files` already present at `to`.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Plan the move from the legacy layouts folder to the standalone home.
|
|
21
|
-
*
|
|
22
|
-
* @param {Object} options
|
|
23
|
-
* @param {string} options.outputDir Docs output dir (owns the legacy folder).
|
|
24
|
-
* @param {string} options.layoutsDir Destination layouts folder.
|
|
25
|
-
* @param {string[]} [options.legacyFiles] Paths relative to the legacy folder.
|
|
26
|
-
* @param {string[]} [options.targetFiles] Paths relative to the destination folder.
|
|
27
|
-
* @returns {LayoutsMigrationPlan}
|
|
28
|
-
*/
|
|
29
|
-
export function planLayoutsMigration(options = {}) {
|
|
30
|
-
const { outputDir, layoutsDir, legacyFiles, targetFiles } = options;
|
|
31
|
-
|
|
32
|
-
const from = legacyLayoutsDir(outputDir);
|
|
33
|
-
const to = normalizeLayoutsDir(layoutsDir);
|
|
34
|
-
|
|
35
|
-
const files = toSortedPaths(legacyFiles);
|
|
36
|
-
const present = new Set(toSortedPaths(targetFiles));
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
required: files.length > 0 && from !== to,
|
|
40
|
-
from,
|
|
41
|
-
to,
|
|
42
|
-
files,
|
|
43
|
-
overwrites: files.filter((file) => present.has(file)),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Normalize, de-duplicate and sort a list of relative paths so a plan is
|
|
49
|
-
* deterministic regardless of directory-read order.
|
|
50
|
-
*
|
|
51
|
-
* @param {unknown} paths
|
|
52
|
-
* @returns {string[]}
|
|
53
|
-
*/
|
|
54
|
-
function toSortedPaths(paths) {
|
|
55
|
-
if (!Array.isArray(paths)) return [];
|
|
56
|
-
const normalized = paths
|
|
57
|
-
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
58
|
-
.map((entry) => entry.replace(/[\\/]+/g, "/").replace(/^\/+/, ""));
|
|
59
|
-
return [...new Set(normalized)].sort(compareCodeUnits);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Plain UTF-16 code-unit order (what `sort()` does by default), spelled out so
|
|
64
|
-
* a plan never depends on the runtime locale.
|
|
65
|
-
*
|
|
66
|
-
* @param {string} a
|
|
67
|
-
* @param {string} b
|
|
68
|
-
* @returns {number}
|
|
69
|
-
*/
|
|
70
|
-
function compareCodeUnits(a, b) {
|
|
71
|
-
if (a < b) return -1;
|
|
72
|
-
if (a > b) return 1;
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface LayoutsMigrationResult {
|
|
2
|
-
moved: string[];
|
|
3
|
-
failed: Array<{ file: string; reason: string }>;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export declare function listFilesRecursively(absoluteDir: string): string[];
|
|
7
|
-
|
|
8
|
-
export declare function executeLayoutsMigration(
|
|
9
|
-
root: string,
|
|
10
|
-
plan: { from: string; to: string; files: string[] },
|
|
11
|
-
): LayoutsMigrationResult;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Filesystem side of the legacy layouts migration.
|
|
3
|
-
*
|
|
4
|
-
* The decision of *what* moves lives in the domain planner; this module only
|
|
5
|
-
* reads directories and performs the move, so both halves stay testable on
|
|
6
|
-
* their own.
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
existsSync,
|
|
10
|
-
readdirSync,
|
|
11
|
-
statSync,
|
|
12
|
-
mkdirSync,
|
|
13
|
-
renameSync,
|
|
14
|
-
copyFileSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
} from "node:fs";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* List every file under a directory, as POSIX paths relative to it.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} absoluteDir
|
|
23
|
-
* @returns {string[]} Empty when the directory is absent or unreadable.
|
|
24
|
-
*/
|
|
25
|
-
export function listFilesRecursively(absoluteDir) {
|
|
26
|
-
if (!absoluteDir || !existsSync(absoluteDir)) return [];
|
|
27
|
-
|
|
28
|
-
const found = [];
|
|
29
|
-
const walk = (currentDir, prefix) => {
|
|
30
|
-
let entries;
|
|
31
|
-
try {
|
|
32
|
-
entries = readdirSync(currentDir);
|
|
33
|
-
} catch {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
for (const entry of entries) {
|
|
37
|
-
const absolute = path.join(currentDir, entry);
|
|
38
|
-
const relative = prefix ? `${prefix}/${entry}` : entry;
|
|
39
|
-
let isDirectory = false;
|
|
40
|
-
try {
|
|
41
|
-
isDirectory = statSync(absolute).isDirectory();
|
|
42
|
-
} catch {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (isDirectory) walk(absolute, relative);
|
|
46
|
-
else found.push(relative);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
walk(absoluteDir, "");
|
|
51
|
-
return found.sort(compareCodeUnits);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Plain UTF-16 code-unit order (what `sort()` does by default), spelled out so
|
|
56
|
-
* the listing never depends on the runtime locale.
|
|
57
|
-
*
|
|
58
|
-
* @param {string} a
|
|
59
|
-
* @param {string} b
|
|
60
|
-
* @returns {number}
|
|
61
|
-
*/
|
|
62
|
-
function compareCodeUnits(a, b) {
|
|
63
|
-
if (a < b) return -1;
|
|
64
|
-
if (a > b) return 1;
|
|
65
|
-
return 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Move one file, falling back to copy + delete when rename is not possible
|
|
70
|
-
* (different volumes, or a locked handle on Windows).
|
|
71
|
-
*
|
|
72
|
-
* @param {string} fromAbsolute
|
|
73
|
-
* @param {string} toAbsolute
|
|
74
|
-
*/
|
|
75
|
-
function moveFile(fromAbsolute, toAbsolute) {
|
|
76
|
-
mkdirSync(path.dirname(toAbsolute), { recursive: true });
|
|
77
|
-
try {
|
|
78
|
-
renameSync(fromAbsolute, toAbsolute);
|
|
79
|
-
} catch {
|
|
80
|
-
copyFileSync(fromAbsolute, toAbsolute);
|
|
81
|
-
rmSync(fromAbsolute, { force: true });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Execute a migration plan produced by `planLayoutsMigration`.
|
|
87
|
-
*
|
|
88
|
-
* Files are moved one by one so a single failure is reported without aborting
|
|
89
|
-
* the rest; the legacy folder is removed only once it is genuinely empty.
|
|
90
|
-
*
|
|
91
|
-
* @param {string} root Project root the plan's paths are relative to.
|
|
92
|
-
* @param {{ from: string, to: string, files: string[] }} plan
|
|
93
|
-
* @returns {{ moved: string[], failed: Array<{ file: string, reason: string }> }}
|
|
94
|
-
*/
|
|
95
|
-
export function executeLayoutsMigration(root, plan) {
|
|
96
|
-
const moved = [];
|
|
97
|
-
const failed = [];
|
|
98
|
-
|
|
99
|
-
const fromRoot = path.join(root, ...plan.from.split("/"));
|
|
100
|
-
const toRoot = path.join(root, ...plan.to.split("/"));
|
|
101
|
-
|
|
102
|
-
for (const file of plan.files) {
|
|
103
|
-
const segments = file.split("/");
|
|
104
|
-
try {
|
|
105
|
-
moveFile(path.join(fromRoot, ...segments), path.join(toRoot, ...segments));
|
|
106
|
-
moved.push(file);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (failed.length === 0 && listFilesRecursively(fromRoot).length === 0) {
|
|
113
|
-
rmSync(fromRoot, { recursive: true, force: true });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { moved, failed };
|
|
117
|
-
}
|