@gitpagedocs/cli 1.1.68 → 1.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/ai/application/ai-command.ts +1 -1
  2. package/ai/application/docs-pattern.ts +9 -5
  3. package/ai/application/resolve-chat-credentials.ts +13 -6
  4. package/ai/core/models/parse-ai-cli-config.ts +6 -8
  5. package/application/home/handler.mjs +3 -1
  6. package/application/ports/cli-runtime-ports.ts +2 -0
  7. package/application/report/config-only-reporter.mjs +2 -5
  8. package/builders/config-orchestrator.mjs +4 -7
  9. package/builders/language-bundles-builder.mjs +4 -4
  10. package/builders/root-config-builder.mjs +1 -1
  11. package/content/docs-en.mjs +4 -4
  12. package/content/docs-es.mjs +4 -4
  13. package/content/docs-pt.mjs +4 -4
  14. package/contracts/doc-versions.mjs +1 -1
  15. package/contracts/langs-paths.mjs +8 -5
  16. package/contracts/layouts-paths.mjs +4 -4
  17. package/contracts/path-segments.d.mts +2 -0
  18. package/contracts/path-segments.mjs +15 -0
  19. package/data/default-site-config.mjs +9 -2
  20. package/domain/services/layouts-migration.mjs +15 -1
  21. package/home/home-file-writer.mjs +2 -3
  22. package/home/templates.mjs +5 -2
  23. package/infrastructure/home/runtime.mjs +2 -2
  24. package/package.json +3 -3
  25. package/presentation/commands/config-info.ts +44 -9
  26. package/presentation/commands/diagnostics.ts +30 -20
  27. package/presentation/commands/docs.ts +7 -2
  28. package/presentation/commands/pages.ts +4 -2
  29. package/presentation/options/parser.ts +46 -45
  30. package/presentation/ui/prompts.ts +6 -5
  31. package/runtime/doc-path-resolver.mjs +8 -6
  32. package/runtime/exec.mjs +168 -0
  33. package/runtime/git-ops.mjs +22 -20
  34. package/runtime/layouts-migrator.mjs +15 -1
  35. package/runtime/output.mjs +88 -58
  36. package/runtime/workflow.mjs +3 -1
  37. package/start.mjs +8 -6
@@ -5,7 +5,7 @@
5
5
  import { ILLMProvider } from '../core/ports/illm-provider';
6
6
 
7
7
  export class AiCommandService {
8
- constructor(private provider: ILLMProvider) { }
8
+ constructor(private readonly provider: ILLMProvider) { }
9
9
 
10
10
  /**
11
11
  * Executes the AI Documentation Generation for a specific content
@@ -14,21 +14,25 @@ export interface AiDocPage {
14
14
  body: string;
15
15
  }
16
16
 
17
- const PAGE_DELIMITER = /^===\s*PAGE:\s*(.+?)\s*\|\s*(.*?)\s*===\s*$/;
17
+ // Slug is everything up to the first `|`, title everything up to the closing
18
+ // `===`; both are trimmed by the caller. Kept free of overlapping quantifiers
19
+ // so a hostile line cannot make the match super-linear.
20
+ const PAGE_DELIMITER = /^===\s*PAGE:([^|]*)\|(.*)===\s*$/;
18
21
 
19
22
  /** Turn an arbitrary heading into a safe lowercase-kebab slug. */
20
23
  export function slugify(raw: string): string {
21
24
  const slug = (raw ?? "")
22
25
  .toLowerCase()
23
26
  .normalize("NFKD")
24
- .replace(/[^a-z0-9]+/g, "-")
25
- .replace(/^-+|-+$/g, "")
27
+ .split(/[^a-z0-9]+/)
28
+ .filter(Boolean)
29
+ .join("-")
26
30
  .slice(0, 60);
27
31
  return slug || "page";
28
32
  }
29
33
 
30
34
  export function parseAiPages(markdown: string): AiDocPage[] {
31
- const text = (markdown ?? "").replace(/\r\n/g, "\n").trim();
35
+ const text = (markdown ?? "").replaceAll("\r\n", "\n").trim();
32
36
  if (!text) return [];
33
37
 
34
38
  const pages: AiDocPage[] = [];
@@ -41,7 +45,7 @@ export function parseAiPages(markdown: string): AiDocPage[] {
41
45
  };
42
46
 
43
47
  for (const line of text.split("\n")) {
44
- const match = line.match(PAGE_DELIMITER);
48
+ const match = PAGE_DELIMITER.exec(line);
45
49
  if (match) {
46
50
  flush();
47
51
  const rawSlug = match[1].trim();
@@ -2,6 +2,7 @@ import { legacyProviderToCatalogId, resolveApiKeyFromEnv } from "@gitpagedocs/to
2
2
  import type { AiProviderId } from "@gitpagedocs/tools/ports";
3
3
  import { PROVIDER_CATALOG } from "@gitpagedocs/tools";
4
4
  import { AiConfigFileRepository } from "../infrastructure/ai-config-file";
5
+ import type { AiCliConfig } from "../core/models/ai-cli-config";
5
6
 
6
7
  export interface ResolvedChatCredentials {
7
8
  readonly providerId: AiProviderId;
@@ -35,12 +36,7 @@ export async function resolveChatCredentials(
35
36
  const configRepo = input.configRepo ?? new AiConfigFileRepository({ cwd: input.cwd });
36
37
  const stored = await configRepo.read().catch(() => null);
37
38
 
38
- const providerId = input.providerOverride
39
- ? legacyProviderToCatalogId(input.providerOverride)
40
- : stored
41
- ? legacyProviderToCatalogId(stored.ai.provider)
42
- : firstProviderFromEnv(env) ?? "openai";
43
-
39
+ const providerId = resolveProviderId(input.providerOverride, stored, env);
44
40
  const spec = PROVIDER_CATALOG[providerId];
45
41
  const model = input.modelOverride?.trim() || stored?.ai.model?.trim() || spec.defaultModel;
46
42
  const baseUrl = stored?.ai.baseUrl?.trim() || undefined;
@@ -52,6 +48,17 @@ export async function resolveChatCredentials(
52
48
  return { providerId, model, apiKey, baseUrl };
53
49
  }
54
50
 
51
+ /** Explicit override > stored config > first provider with an env key > openai. */
52
+ function resolveProviderId(
53
+ override: string | undefined,
54
+ stored: AiCliConfig | null,
55
+ env: Readonly<Record<string, string | undefined>>,
56
+ ): AiProviderId {
57
+ if (override) return legacyProviderToCatalogId(override);
58
+ if (stored) return legacyProviderToCatalogId(stored.ai.provider);
59
+ return firstProviderFromEnv(env) ?? "openai";
60
+ }
61
+
55
62
  function firstProviderFromEnv(
56
63
  env: Readonly<Record<string, string | undefined>>,
57
64
  ): AiProviderId | undefined {
@@ -1,8 +1,10 @@
1
1
  import { AI_MODEL_DEFAULTS, type AiProviderId } from "../../config";
2
2
  import type { AiCliConfig } from "./ai-cli-config";
3
3
 
4
- const SUPPORTED_PROVIDERS = Object.keys(AI_MODEL_DEFAULTS) as AiProviderId[];
5
- const SUPPORTED_LANGUAGES: ReadonlyArray<AiCliConfig["ai"]["languages"][number]> = ["pt", "en", "es"];
4
+ type SupportedLanguage = AiCliConfig["ai"]["languages"][number];
5
+
6
+ const SUPPORTED_PROVIDERS: ReadonlySet<string> = new Set(Object.keys(AI_MODEL_DEFAULTS));
7
+ const SUPPORTED_LANGUAGES: ReadonlySet<string> = new Set<SupportedLanguage>(["pt", "en", "es"]);
6
8
 
7
9
  /** Fallbacks for optional fields, mirroring the interactive prompt defaults. */
8
10
  const DEFAULT_LANGUAGES: AiCliConfig["ai"]["languages"] = ["en", "pt", "es"];
@@ -27,17 +29,13 @@ function asPaths(value: unknown): string[] {
27
29
 
28
30
  function asLanguages(value: unknown): AiCliConfig["ai"]["languages"] {
29
31
  const languages = Array.isArray(value)
30
- ? value.filter((language): language is AiCliConfig["ai"]["languages"][number] =>
31
- SUPPORTED_LANGUAGES.includes(language as AiCliConfig["ai"]["languages"][number]),
32
- )
32
+ ? value.filter((language): language is SupportedLanguage => isString(language) && SUPPORTED_LANGUAGES.has(language))
33
33
  : [];
34
34
  return languages.length > 0 ? languages : [...DEFAULT_LANGUAGES];
35
35
  }
36
36
 
37
37
  function asProviderId(value: unknown): AiProviderId | null {
38
- return isString(value) && SUPPORTED_PROVIDERS.includes(value as AiProviderId)
39
- ? (value as AiProviderId)
40
- : null;
38
+ return isString(value) && SUPPORTED_PROVIDERS.has(value) ? (value as AiProviderId) : null;
41
39
  }
42
40
 
43
41
  /**
@@ -1,6 +1,7 @@
1
1
  /** Home distribution use case - static site + auxiliary files */
2
2
 
3
3
  import { STATIC_OUTPUT_DIR, ARTIFACTS_DIR } from "../../home/constants.mjs";
4
+ import { trimSlashes } from "../../contracts/path-segments.mjs";
4
5
 
5
6
  /**
6
7
  * Execute home distribution use case
@@ -30,7 +31,7 @@ export async function executeHome(params, runtime) {
30
31
  runtime.ensureDirEmpty(root, outputDir);
31
32
  }
32
33
 
33
- const pathSegment = basePath ? basePath.replace(/^\/+|\/+$/g, "") : "";
34
+ const pathSegment = basePath ? trimSlashes(basePath) : "";
34
35
  const buildEnv = {
35
36
  ...process.env,
36
37
  GITHUB_ACTIONS: "true",
@@ -83,3 +84,4 @@ export async function executeHome(params, runtime) {
83
84
  runtime.logInfo(`Serve: cd ${cdDir} && npx serve .`);
84
85
  runtime.logInfo(`Docker: cd ${cdDir} && docker build -t gitpagedocshome . && docker run -p 3000:80 gitpagedocshome`);
85
86
  }
87
+
@@ -16,6 +16,8 @@ export interface LayoutDefinition {
16
16
 
17
17
  export interface BuiltConfigArtifacts {
18
18
  rootConfig: unknown;
19
+ /** UI-string bundle per language, written to `<outputDir>/langs/<lang>.json`. */
20
+ languageBundles: Record<string, unknown>;
19
21
  layoutsConfig: unknown;
20
22
  fallbackLayoutsConfig: unknown;
21
23
  docs?: Record<string, Record<string, string>>;
@@ -8,7 +8,7 @@ export function reportConfigOnlySuccess(options) {
8
8
  const langs = languageArtifactPaths(options.outputDir);
9
9
  return [
10
10
  `Generated: ${options.outputDir}/ (config-only)`,
11
- `UI strings: ${langs.manifest} + ${langs.dir}/{${SUPPORTED_LANGUAGES.join(",")}}.json`,
11
+ `UI strings: ${langs.dir}/{${SUPPORTED_LANGUAGES.join(",")}}.json (enable/disable each language in ${langs.config} -> site.languages)`,
12
12
  "No index.html/index.js generated.",
13
13
  ];
14
14
  }
@@ -51,10 +51,7 @@ export function reportPrebuiltDetected() {
51
51
  }
52
52
 
53
53
  export function reportAll(options, prebuiltDetected) {
54
- const lines = [];
55
- lines.push(...reportConfigOnlySuccess(options));
56
- lines.push(...reportLayoutConfig(options));
57
- lines.push(...reportRenderingUrl(options));
54
+ const lines = [...reportConfigOnlySuccess(options), ...reportLayoutConfig(options), ...reportRenderingUrl(options)];
58
55
  if (options.shouldPush) {
59
56
  lines.push(...reportPushMode());
60
57
  }
@@ -9,14 +9,12 @@ import { buildLanguageArtifacts } from "./language-bundles-builder.mjs";
9
9
 
10
10
  /**
11
11
  * Build all config artifacts (root, languages, layouts, versions, docs).
12
- * @param {object} options - { useLocalLayoutConfig, githubOwner, githubRepo, root }
13
- * @returns {object} { rootConfig, languageManifest, languageBundles, layoutsConfig, fallbackLayoutsConfig, docs, docsHtml, versionConfigs }
12
+ * @param {object} options - { useLocalLayoutConfig, githubOwner, githubRepo, layoutsDir }
13
+ * @returns {object} { rootConfig, languageBundles, layoutsConfig, fallbackLayoutsConfig, docs, docsHtml, versionConfigs }
14
14
  */
15
15
  export function buildConfigArtifacts(options = {}) {
16
- const root = options.root ?? process.cwd();
17
-
18
- const rootConfig = buildRootConfig(options);
19
- const { languageManifest, languageBundles } = buildLanguageArtifacts();
16
+ const { languageToggles, languageBundles } = buildLanguageArtifacts();
17
+ const rootConfig = buildRootConfig({ ...options, languageToggles });
20
18
  const layoutsConfig = { layouts: LAYOUTS };
21
19
  const fallbackLayoutsConfig = { layouts: FALLBACK_LAYOUTS };
22
20
 
@@ -27,7 +25,6 @@ export function buildConfigArtifacts(options = {}) {
27
25
 
28
26
  return {
29
27
  rootConfig,
30
- languageManifest,
31
28
  languageBundles,
32
29
  layoutsConfig,
33
30
  fallbackLayoutsConfig,
@@ -1,5 +1,5 @@
1
- /** Build the language manifest + per-language UI string bundles for gitpagedocs */
2
- import { buildLanguageManifest, splitLanguageBundles } from "@gitpagedocs/tools/i18n";
1
+ /** Build the `site.languages` toggles + per-language UI string bundles for gitpagedocs */
2
+ import { buildLanguageToggles, splitLanguageBundles } from "@gitpagedocs/tools/i18n";
3
3
  import { SUPPORTED_LANGUAGES } from "../contracts/languages.mjs";
4
4
  import { defaultLangMenu } from "../data/i18n-langmenu.mjs";
5
5
  import { defaultTranslations } from "../data/i18n-translations.mjs";
@@ -12,13 +12,13 @@ function assertLanguageStrings(languages) {
12
12
  }
13
13
 
14
14
  /**
15
- * @returns {{ languageManifest: { languages: string[] }, languageBundles: Record<string, object> }}
15
+ * @returns {{ languageToggles: Record<string, boolean>, languageBundles: Record<string, object> }}
16
16
  */
17
17
  export function buildLanguageArtifacts() {
18
18
  const languages = [...SUPPORTED_LANGUAGES];
19
19
  assertLanguageStrings(languages);
20
20
  return {
21
- languageManifest: buildLanguageManifest(languages),
21
+ languageToggles: buildLanguageToggles(languages),
22
22
  languageBundles: splitLanguageBundles(languages, defaultLangMenu, defaultTranslations),
23
23
  };
24
24
  }
@@ -34,7 +34,7 @@ export function buildRootConfig(options = {}) {
34
34
  "source-viewer-path": sourceViewerPath,
35
35
  }));
36
36
 
37
- const baseSiteConfig = getDefaultSiteConfig(projectLink);
37
+ const baseSiteConfig = getDefaultSiteConfig(projectLink, options.languageToggles);
38
38
 
39
39
  return {
40
40
  site: {
@@ -175,7 +175,7 @@ The version config can render a **Source code** container through \`routes-sourc
175
175
  - \`name\`, \`defaultLanguage\`
176
176
  - \`docsVersion\`, \`rendering\`, \`ThemeDefault\`, \`ThemeModeDefault\`
177
177
  - \`ProjectLink\`, \`layoutsConfigPathOficial\`, \`layoutsConfigPath\`
178
- - Languages and UI strings: \`gitpagedocs/langs.json\` + \`gitpagedocs/langs/<lang>.json\`
178
+ - Languages: \`site.languages\` (enable/disable each one); UI strings: \`gitpagedocs/langs/<lang>.json\`
179
179
 
180
180
  ## Environment variables
181
181
 
@@ -319,11 +319,11 @@ Supported adapters:
319
319
 
320
320
  Runtime configuration lives in \`gitpagedocs/config.json\`. UI text lives next to it, one file per language.
321
321
 
322
- ## Languages (\`langs.json\` + \`langs/\`)
322
+ ## Languages (\`site.languages\` + \`langs/\`)
323
323
 
324
- - \`gitpagedocs/langs.json\` lists the shipped languages in menu order: \`{ "languages": ["en", "pt", "es"] }\`.
324
+ - \`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.
325
325
  - \`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\`).
326
- - To add a language, create \`langs/<lang>.json\` and append its code to \`langs.json\`.
326
+ - 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.
327
327
  - 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.
328
328
 
329
329
  ## \`site\` section
@@ -159,7 +159,7 @@ El config de version puede renderizar un contenedor **Codigo fuente** via \`rout
159
159
  - \`name\`, \`defaultLanguage\`
160
160
  - \`docsVersion\`, \`rendering\`, \`ThemeDefault\`, \`ThemeModeDefault\`
161
161
  - \`ProjectLink\`, \`layoutsConfigPathOficial\`, \`layoutsConfigPath\`
162
- - Idiomas y textos de la UI: \`gitpagedocs/langs.json\` + \`gitpagedocs/langs/<lang>.json\`
162
+ - Idiomas: \`site.languages\` (activa/desactiva cada uno); textos de la UI: \`gitpagedocs/langs/<lang>.json\`
163
163
 
164
164
  ## Variables de entorno
165
165
 
@@ -170,11 +170,11 @@ El config de version puede renderizar un contenedor **Codigo fuente** via \`rout
170
170
 
171
171
  La configuracion de runtime esta en \`gitpagedocs/config.json\`. Los textos de la UI viven al lado, un archivo por idioma.
172
172
 
173
- ## Idiomas (\`langs.json\` + \`langs/\`)
173
+ ## Idiomas (\`site.languages\` + \`langs/\`)
174
174
 
175
- - \`gitpagedocs/langs.json\` lista los idiomas disponibles en el orden del menu: \`{ "languages": ["en", "pt", "es"] }\`.
175
+ - \`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.
176
176
  - \`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\`).
177
- - Para agregar un idioma, crea \`langs/<lang>.json\` y agrega su codigo a \`langs.json\`.
177
+ - 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.
178
178
  - 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.
179
179
 
180
180
  ## Seccion \`site\`
@@ -159,7 +159,7 @@ O config da versao pode renderizar um container **Codigo fonte** via \`routes-so
159
159
  - \`name\`, \`defaultLanguage\`
160
160
  - \`docsVersion\`, \`rendering\`, \`ThemeDefault\`, \`ThemeModeDefault\`
161
161
  - \`ProjectLink\`, \`layoutsConfigPathOficial\`, \`layoutsConfigPath\`
162
- - Idiomas e textos da UI: \`gitpagedocs/langs.json\` + \`gitpagedocs/langs/<lang>.json\`
162
+ - Idiomas: \`site.languages\` (liga/desliga cada um); textos da UI: \`gitpagedocs/langs/<lang>.json\`
163
163
 
164
164
  ## Variaveis de ambiente
165
165
 
@@ -170,11 +170,11 @@ O config da versao pode renderizar um container **Codigo fonte** via \`routes-so
170
170
 
171
171
  A configuracao de runtime fica em \`gitpagedocs/config.json\`. Os textos da UI ficam ao lado, um arquivo por idioma.
172
172
 
173
- ## Idiomas (\`langs.json\` + \`langs/\`)
173
+ ## Idiomas (\`site.languages\` + \`langs/\`)
174
174
 
175
- - \`gitpagedocs/langs.json\` lista os idiomas disponiveis na ordem do menu: \`{ "languages": ["en", "pt", "es"] }\`.
175
+ - \`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.
176
176
  - \`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\`).
177
- - Para adicionar um idioma, crie \`langs/<lang>.json\` e inclua o codigo em \`langs.json\`.
177
+ - 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.
178
178
  - 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.
179
179
 
180
180
  ## Secao \`site\`
@@ -1,3 +1,3 @@
1
- export const PACKAGE_VERSION = "1.1.68";
1
+ export const PACKAGE_VERSION = "1.1.69";
2
2
 
3
3
  export const DOC_VERSIONS = [PACKAGE_VERSION];
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Canonical locations for the language artifacts inside the docs output dir:
3
3
  *
4
- * <outputDir>/langs.json manifest listing the shipped languages
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
- * The file names come from @gitpagedocs/tools so the generator and the viewer
8
- * never disagree about where the bundles live.
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.
9
11
  */
10
12
  import { LANGS_DIRNAME, LANGS_MANIFEST_FILENAME } from "@gitpagedocs/tools/i18n";
11
13
 
@@ -13,12 +15,13 @@ import { LANGS_DIRNAME, LANGS_MANIFEST_FILENAME } from "@gitpagedocs/tools/i18n"
13
15
  * Repo-relative POSIX paths of the language artifacts for a docs output dir.
14
16
  *
15
17
  * @param {string} outputDir Docs output dir (e.g. `gitpagedocs`).
16
- * @returns {{ manifest: string, dir: string, bundle: (language: string) => string }}
18
+ * @returns {{ config: string, legacyManifest: string, dir: string, bundle: (language: string) => string }}
17
19
  */
18
20
  export function languageArtifactPaths(outputDir) {
19
21
  const dir = `${outputDir}/${LANGS_DIRNAME}`;
20
22
  return {
21
- manifest: `${outputDir}/${LANGS_MANIFEST_FILENAME}`,
23
+ config: `${outputDir}/config.json`,
24
+ legacyManifest: `${outputDir}/${LANGS_MANIFEST_FILENAME}`,
22
25
  dir,
23
26
  bundle: (language) => `${dir}/${language}.json`,
24
27
  };
@@ -1,3 +1,5 @@
1
+ import { trimSlashes } from "./path-segments.mjs";
2
+
1
3
  /**
2
4
  * Canonical locations for layout artifacts.
3
5
  *
@@ -34,10 +36,8 @@ export const LEGACY_LAYOUTS_SUBDIR = "layouts";
34
36
  * @returns {string} Normalized path, or `DEFAULT_LAYOUTS_DIR` when empty.
35
37
  */
36
38
  export function normalizeLayoutsDir(dir) {
37
- const normalized = String(dir ?? "")
38
- .replace(/[\\/]+/g, "/")
39
- .replace(/^\/+|\/+$/g, "")
40
- .trim();
39
+ const collapsed = String(dir ?? "").replace(/[\\/]+/g, "/");
40
+ const normalized = trimSlashes(collapsed).trim();
41
41
  return normalized || DEFAULT_LAYOUTS_DIR;
42
42
  }
43
43
 
@@ -0,0 +1,2 @@
1
+ /** Strip every leading and trailing `/` from a path segment. */
2
+ export function trimSlashes(value: string): string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Strip every leading and trailing `/` from a user-supplied path segment in a
3
+ * single forward/backward scan. Same result as `.replace(/^\/+|\/+$/g, "")`
4
+ * without the super-linear backtracking that regex incurs on long slash runs.
5
+ *
6
+ * @param {string} value Raw segment such as "/docs/" or "///".
7
+ * @returns {string} The segment without surrounding slashes ("" when only slashes).
8
+ */
9
+ export function trimSlashes(value) {
10
+ let start = 0;
11
+ let end = value.length;
12
+ while (start < end && value[start] === "/") start += 1;
13
+ while (end > start && value[end - 1] === "/") end -= 1;
14
+ return value.slice(start, end);
15
+ }
@@ -1,8 +1,15 @@
1
- /** Site chrome defaults. The language list and UI strings live in langs.json + langs/. */
2
- export function getDefaultSiteConfig(projectLink) {
1
+ /**
2
+ * Site chrome defaults. `languages` enables (true) / disables (false) each
3
+ * language; the UI strings themselves live in langs/<lang>.json.
4
+ */
5
+ import { buildLanguageToggles } from "@gitpagedocs/tools/i18n";
6
+ import { SUPPORTED_LANGUAGES } from "../contracts/languages.mjs";
7
+
8
+ export function getDefaultSiteConfig(projectLink, languageToggles = buildLanguageToggles(SUPPORTED_LANGUAGES)) {
3
9
  return {
4
10
  name: "Git Pages Docs",
5
11
  defaultLanguage: "en",
12
+ languages: { ...languageToggles },
6
13
  HideThemeSelector: false,
7
14
  ThemeDefault: "aurora-dark",
8
15
  ThemeModeDefault: "dark",
@@ -56,5 +56,19 @@ function toSortedPaths(paths) {
56
56
  const normalized = paths
57
57
  .filter((entry) => typeof entry === "string" && entry.trim().length > 0)
58
58
  .map((entry) => entry.replace(/[\\/]+/g, "/").replace(/^\/+/, ""));
59
- return [...new Set(normalized)].sort();
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;
60
74
  }
@@ -1,8 +1,7 @@
1
1
  /** Write .env, Dockerfile, README to home output directory */
2
2
 
3
3
  import path from "node:path";
4
- import { mkdir } from "node:fs/promises";
5
- import { writeFile } from "node:fs/promises";
4
+ import { mkdir, writeFile } from "node:fs/promises";
6
5
  import { getEnvTemplate, getDockerfileTemplate, getReadmeTemplate } from "./templates.mjs";
7
6
 
8
7
  /**
@@ -16,5 +15,5 @@ export async function writeHomeFiles(root, outputDir, opts = {}) {
16
15
 
17
16
  await writeFile(path.join(basePath, ".env"), getEnvTemplate(opts), "utf-8");
18
17
  await writeFile(path.join(basePath, "Dockerfile"), getDockerfileTemplate(), "utf-8");
19
- await writeFile(path.join(basePath, "README.md"), getReadmeTemplate({ outputDir }), "utf-8");
18
+ await writeFile(path.join(basePath, "README.md"), getReadmeTemplate(), "utf-8");
20
19
  }
@@ -22,8 +22,11 @@ CMD ["nginx", "-g", "daemon off;"]
22
22
  `;
23
23
  }
24
24
 
25
- export function getReadmeTemplate({ outputDir = "gitpagedocshome" }) {
26
- const cdDir = outputDir === "." || outputDir === "./" ? "." : outputDir;
25
+ /**
26
+ * README shipped next to the static export. Every command runs from the
27
+ * folder the file lives in, so it never needs to know the output dir name.
28
+ */
29
+ export function getReadmeTemplate() {
27
30
  return `# GitPageDocs Home
28
31
 
29
32
  Pre-built GitPageDocs static site by Vidigal-code.
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { cpSync, existsSync, readdirSync, rmSync } from "node:fs";
3
- import { execSync } from "node:child_process";
3
+ import { runExecutable } from "../../runtime/exec.mjs";
4
4
  import { writeConfigOnlyOutput, writeText } from "../../runtime/output.mjs";
5
5
  import { writeHomeFiles } from "../../home/home-file-writer.mjs";
6
6
  import { logInfo, logSuccess } from "../../ui/logger.mjs";
@@ -17,7 +17,7 @@ function copyRecursive(src, dest) {
17
17
  }
18
18
 
19
19
  function runNextBuild(root, env) {
20
- execSync("npx next build frontend", { cwd: root, env, stdio: "inherit" });
20
+ runExecutable("npx", ["next", "build", "frontend"], { cwd: root, env, stdio: "inherit" });
21
21
  }
22
22
 
23
23
  export function createHomeRuntime() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitpagedocs/cli",
3
- "version": "1.1.68",
3
+ "version": "1.1.69",
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": "^1.1.68",
33
- "@gitpagedocs/tools": "^1.1.68",
32
+ "@gitpagedocs/mcp": "^1.1.69",
33
+ "@gitpagedocs/tools": "^1.1.69",
34
34
  "tsx": "^4.21.0"
35
35
  },
36
36
  "repository": {
@@ -1,19 +1,53 @@
1
1
  import path from "node:path";
2
2
  import { readFile } from "node:fs/promises";
3
- import { DEFAULT_LANGS_MANIFEST_PATH, defaultConfigLoader, isAppError, parseLanguageManifest } from "@gitpagedocs/tools";
3
+ import {
4
+ DEFAULT_LANGS_MANIFEST_PATH,
5
+ defaultConfigLoader,
6
+ getEnabledLanguages,
7
+ isAppError,
8
+ parseLanguageManifest,
9
+ parseLanguageToggles,
10
+ } from "@gitpagedocs/tools";
4
11
  import type { CommandContext } from "./run-command";
5
12
 
6
13
  const UNKNOWN = "—";
7
14
 
8
- /** Languages from `gitpagedocs/langs.json`, else the legacy inline `site.supportedLanguages`. */
9
- async function resolveLanguages(cwd: string, site: Record<string, unknown>): Promise<string[]> {
15
+ interface LanguageSummary {
16
+ enabled: string[];
17
+ disabled: string[];
18
+ }
19
+
20
+ /** The 1.1.68 manifest, read only for sites whose config.json predates `site.languages`. */
21
+ async function readLegacyManifestLanguages(cwd: string): Promise<string[]> {
10
22
  try {
11
23
  const manifest = parseLanguageManifest(JSON.parse(await readFile(path.join(cwd, DEFAULT_LANGS_MANIFEST_PATH), "utf-8")));
12
24
  if (manifest) return manifest.languages;
13
25
  } catch {
14
- // No manifest: fall back to the inline list below.
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> {
33
+ const toggles = parseLanguageToggles(site.languages);
34
+ if (toggles) {
35
+ const enabled = getEnabledLanguages(toggles);
36
+ return { enabled, disabled: Object.keys(toggles).filter((language) => !enabled.includes(language)) };
15
37
  }
16
- return Array.isArray(site.supportedLanguages) ? (site.supportedLanguages as string[]) : [];
38
+ if (Array.isArray(site.supportedLanguages)) {
39
+ return { enabled: site.supportedLanguages as string[], disabled: [] };
40
+ }
41
+ return { enabled: await readLegacyManifestLanguages(cwd), disabled: [] };
42
+ }
43
+
44
+ function formatList(languages: string[]): string {
45
+ return languages.length > 0 ? languages.join(", ") : UNKNOWN;
46
+ }
47
+
48
+ /** Scalars print as-is; anything else (missing, object) prints as unknown instead of "[object Object]". */
49
+ function formatScalar(value: unknown): string {
50
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : UNKNOWN;
17
51
  }
18
52
 
19
53
  /** `gitpagedocs config` — resolve and summarize the active gitpagedocs config. */
@@ -21,13 +55,14 @@ export async function runConfig(ctx: CommandContext): Promise<void> {
21
55
  try {
22
56
  const { config, sourcePath, extension } = await defaultConfigLoader.loadGitPageDocsConfig(ctx.cwd);
23
57
  const site = (config as { site?: Record<string, unknown> }).site ?? {};
24
- const languages = await resolveLanguages(ctx.cwd, site);
58
+ const languages = await summarizeLanguages(ctx.cwd, site);
25
59
  // eslint-disable-next-line no-console
26
60
  console.log(
27
61
  `\n Config: ${path.relative(ctx.cwd, sourcePath)} (${extension})\n` +
28
- ` site name : ${String(site.name ?? UNKNOWN)}\n` +
29
- ` default language : ${String(site.defaultLanguage ?? UNKNOWN)}\n` +
30
- ` languages : ${languages.length > 0 ? languages.join(", ") : UNKNOWN}\n`,
62
+ ` site name : ${formatScalar(site.name)}\n` +
63
+ ` default language : ${formatScalar(site.defaultLanguage)}\n` +
64
+ ` languages : ${formatList(languages.enabled)}\n` +
65
+ ` disabled : ${formatList(languages.disabled)}\n`,
31
66
  );
32
67
  } catch (error) {
33
68
  const message = isAppError(error) ? error.message : String(error);