@gitpagedocs/cli 0.0.3 → 0.0.5

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 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`; a legacy
173
- `.gitpagedocsconfig` in the repository root is migrated automatically on the next
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 legacy plaintext `"apiKey"` still present in an old file is sealed into the vault and
204
- removed from the file the next time the config is read.
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({ cwd: input.cwd });
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
- const current = await this.readFrom(this.getConfigPath());
51
- if (current) return current;
52
- return this.migrateLegacyConfig();
37
+ return this.readFrom(this.getConfigPath());
53
38
  }
54
39
 
55
- /** Delete the stored configuration (secure and legacy locations), wiping
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 removed: string[] = [];
59
- const candidates = new Set([this.getConfigPath(), this.getLegacyConfigPath()].map((p) => path.resolve(p)));
60
- for (const configPath of candidates) {
61
- try {
62
- await fs.rm(configPath);
63
- removed.push(configPath);
64
- } catch {
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: dark ? "1px solid #334155" : "1px solid #E2E8F0",
47
- hoverGlow: dark ? "0 0 0 3px rgba(124, 58, 237, 0.18)" : "0 0 0 4px rgba(109, 40, 217, 0.15)",
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: dark ? "1px solid #334155" : "1px solid #E2E8F0",
53
- backgroundColor: dark ? "#0F172A" : "#FFFFFF",
62
+ border: `1px solid ${colors.cardBorder}`,
63
+ backgroundColor: colors.cardBackground,
54
64
  textAlign: "center",
55
65
  iconColor: colors.secondary,
56
- hoverBorderColor: dark ? "rgba(34, 211, 238, 0.7)" : "rgba(8, 145, 178, 0.7)",
57
- focusBorderColor: dark ? "rgba(124, 58, 237, 0.8)" : "rgba(109, 40, 217, 0.8)",
58
- focusGlow: dark ? "0 0 0 4px rgba(124, 58, 237, 0.18)" : "0 0 0 4px rgba(109, 40, 217, 0.15)",
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",
@@ -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. A legacy plaintext key is migrated and wiped on first unlock.
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\`. There is no \`langs.json\` any more: a manifest left by release 1.1.68 is still read by configs without \`site.languages\`, and the generator removes it.
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:
@@ -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. Una clave heredada en texto plano se migra y se borra en el primer desbloqueo.
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\`. Ya no existe \`langs.json\`: un manifiesto dejado por la version 1.1.68 aun se lee en configs antiguos sin \`site.languages\`, y el generador lo elimina.
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\`:
@@ -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. Uma chave legada em texto puro e migrada e apagada no primeiro desbloqueio.
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\`. Nao existe mais \`langs.json\`: um manifesto deixado pela versao 1.1.68 ainda e lido por configs antigos sem \`site.languages\`, e o gerador o remove.
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\`:
@@ -1,3 +1,3 @@
1
- export const PACKAGE_VERSION = "0.0.3";
1
+ export const PACKAGE_VERSION = "0.0.5";
2
2
 
3
3
  export const DOC_VERSIONS = [PACKAGE_VERSION];
@@ -4,8 +4,6 @@ export {
4
4
  LAYOUTS_CONFIG_FILENAME,
5
5
  LAYOUTS_FALLBACK_CONFIG_FILENAME,
6
6
  LAYOUTS_TEMPLATES_DIRNAME,
7
- LEGACY_LAYOUTS_SUBDIR,
8
7
  normalizeLayoutsDir,
9
- legacyLayoutsDir,
10
8
  layoutsArtifactPaths,
11
9
  } from "./layouts-paths.mjs";
@@ -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
- * `<outputDir>/langs.json` was the 1.1.68 manifest; `site.languages` replaced
8
- * it, so the generator deletes a stale copy. The file names come from
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, LANGS_MANIFEST_FILENAME } from "@gitpagedocs/tools/i18n";
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, legacyManifest: string, dir: string, bundle: (language: string) => 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
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Languages the generator ships UI strings and docs for, in menu order. This
3
- * is the list written to `gitpagedocs/langs.json`; every entry must have a
4
- * `langs/<lang>.json` bundle and a docs content module.
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. `<outputDir>/layouts/` is the legacy location that earlier
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
  *
@@ -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,6 +90,18 @@ 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
107
  aiChatProviderOpenAI: "OpenAI",
@@ -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,6 +227,18 @@ 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
244
  aiChatProviderOpenAI: "OpenAI",
@@ -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,6 +364,18 @@ 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
381
  aiChatProviderOpenAI: "OpenAI",
package/data/urls.mjs CHANGED
@@ -1,12 +1,7 @@
1
- /** Official layout URLs for git-page-docs repository.
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",
3
+ "version": "0.0.5",
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.3",
33
- "@gitpagedocs/tools": "^0.0.3",
32
+ "@gitpagedocs/mcp": "^0.0.5",
33
+ "@gitpagedocs/tools": "^0.0.5",
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({ cwd: ctx.cwd });
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 (and any legacy repo-root copy) together with the
8
- * encrypted key vault, wiping the saved AI credentials, provider and scan paths.
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(ctx: CommandContext): Promise<void> {
11
- const repository = new AiConfigFileRepository({ cwd: ctx.cwd });
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 { readFile } from "node:fs/promises";
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
- /** The 1.1.68 manifest, read only for sites whose config.json predates `site.languages`. */
21
- async function readLegacyManifestLanguages(cwd: string): Promise<string[]> {
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: await readLegacyManifestLanguages(cwd), disabled: [] };
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 = await summarizeLanguages(ctx.cwd, site);
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` +
@@ -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
- * Layout-related prompts: where layouts come from, where they are written, and
3
- * the opt-in migration away from the legacy `<outputDir>/layouts/` folder.
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
- }
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, rmSync } from "node:fs";
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
- removeLegacyLanguageDirs(root, outputDir);
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
- }