@gitpagedocs/cli 1.1.61 → 1.1.62

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
@@ -17,7 +17,9 @@ npx @gitpagedocs/cli
17
17
  | Command | Description |
18
18
  |---|---|
19
19
  | `gitpagedocs` | Generate `gitpagedocs/` config + versioned docs (config-only) |
20
- | `gitpagedocs --layoutconfig` | Also emit local layout templates in `gitpagedocs/layouts/` |
20
+ | `gitpagedocs --layoutconfig` | Also emit local layout templates in `gitpagelayouts/` |
21
+ | `gitpagedocs --layouts-dir <dir>` | Folder for local layouts (default: `gitpagelayouts`) |
22
+ | `gitpagedocs --no-interactive` / `-y` | Never prompt; use flags and defaults |
21
23
  | `gitpagedocs --push --owner <o> --repo <r> [--path <sub>]` | Generate, configure Pages, create workflow, commit + push |
22
24
  | `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`: static site + .env + Dockerfile) |
23
25
  | `gitpagedocs -i` / `--interactive` | Interactive prompts |
@@ -24,6 +24,7 @@ export async function executeConfigOnly(params, runtime) {
24
24
 
25
25
  const artifacts = buildConfigArtifacts({
26
26
  useLocalLayoutConfig: options.useLocalLayoutConfig,
27
+ layoutsDir: options.layoutsDir,
27
28
  githubOwner: sanitizeSegment(options.githubOwner),
28
29
  githubRepo: sanitizeSegment(options.githubRepo),
29
30
  root,
@@ -33,6 +34,7 @@ export async function executeConfigOnly(params, runtime) {
33
34
  root,
34
35
  pkgRoot,
35
36
  outputDir: options.outputDir,
37
+ layoutsDir: options.layoutsDir,
36
38
  artifacts,
37
39
  useLocalLayoutConfig: options.useLocalLayoutConfig,
38
40
  layouts,
@@ -41,6 +41,7 @@ export async function executeHome(params, runtime) {
41
41
 
42
42
  const artifacts = buildConfigArtifacts({
43
43
  useLocalLayoutConfig: false,
44
+ layoutsDir: options.layoutsDir,
44
45
  githubOwner: "",
45
46
  githubRepo: "",
46
47
  root,
@@ -50,6 +51,7 @@ export async function executeHome(params, runtime) {
50
51
  root,
51
52
  pkgRoot,
52
53
  outputDir: ARTIFACTS_DIR,
54
+ layoutsDir: options.layoutsDir,
53
55
  artifacts,
54
56
  useLocalLayoutConfig: false,
55
57
  layouts,
@@ -2,6 +2,8 @@ import type { CliOptions } from "../../domain/models/cli-options";
2
2
 
3
3
  export interface BuildArtifactsInput {
4
4
  useLocalLayoutConfig: boolean;
5
+ /** Repo-relative layouts home the generated config should reference. */
6
+ layoutsDir: string;
5
7
  githubOwner: string;
6
8
  githubRepo: string;
7
9
  root: string;
@@ -44,6 +46,8 @@ export interface ConfigOnlyOutputInput {
44
46
  root: string;
45
47
  pkgRoot: string;
46
48
  outputDir: string;
49
+ /** Repo-relative layouts home local layouts are written to. */
50
+ layoutsDir: string;
47
51
  artifacts: BuiltConfigArtifacts;
48
52
  useLocalLayoutConfig: boolean;
49
53
  layouts: LayoutDefinition[];
@@ -1,16 +1,17 @@
1
1
  /** Config-only command output messages - SRP: single place for all report messages */
2
2
  import { resolveRenderingUrl } from "../../builders/project-links.mjs";
3
+ import { normalizeLayoutsDir } from "../../contracts/layouts-paths.mjs";
3
4
 
4
5
  export function reportConfigOnlySuccess(options) {
5
6
  return [`Generated: ${options.outputDir}/ (config-only)`, "No index.html/index.js generated."];
6
7
  }
7
8
 
8
9
  export function reportLayoutConfig(options) {
10
+ const layoutsDir = normalizeLayoutsDir(options.layoutsDir);
9
11
  if (options.useLocalLayoutConfig) {
10
- return ["Local layouts generated in gitpagedocs/layouts/ (--layoutconfig)."];
11
- } else {
12
- return ["Using official remote layouts config by default (no local gitpagedocs/layouts generated)."];
12
+ return [`Local layouts generated in ${layoutsDir}/ (--layoutconfig).`];
13
13
  }
14
+ return [`Using official remote layouts config by default (no local ${layoutsDir}/ generated).`];
14
15
  }
15
16
 
16
17
  export function reportRenderingUrl(options) {
@@ -1,6 +1,8 @@
1
1
  /** Shared derivation of project URLs from CLI options (single source of truth
2
2
  * for root and version config builders). */
3
3
 
4
+ import { layoutsArtifactPaths } from "../contracts/layouts-paths.mjs";
5
+
4
6
  export const DEFAULT_PROJECT_LINK = "https://github.com/Vidigal-code/git-page-docs";
5
7
  export const DEFAULT_RENDERING_URL = "https://vidigal-code.github.io/git-page-docs/";
6
8
 
@@ -16,6 +18,28 @@ export function resolveRenderingUrl(githubOwner, githubRepo) {
16
18
  : DEFAULT_RENDERING_URL;
17
19
  }
18
20
 
21
+ /**
22
+ * Where the viewer should look for locally generated layouts.
23
+ *
24
+ * Mirrors resolveSourceViewerPath: user repositories use the symbolic HEAD ref
25
+ * so any default branch resolves. Without an owner/repo pair there is no URL to
26
+ * build, so the repo-relative path is returned - the viewer reads it directly in
27
+ * local mode and still discovers it by probing the repository otherwise.
28
+ *
29
+ * @param {string} githubOwner
30
+ * @param {string} githubRepo
31
+ * @param {string} layoutsDir Repo-relative layouts home.
32
+ * @returns {{ config: string, templates: string }}
33
+ */
34
+ export function resolveLayoutsLinks(githubOwner, githubRepo, layoutsDir) {
35
+ const paths = layoutsArtifactPaths(layoutsDir);
36
+ if (!githubOwner || !githubRepo) {
37
+ return { config: paths.config, templates: paths.templates };
38
+ }
39
+ const base = `${resolveProjectLink(githubOwner, githubRepo)}/blob/HEAD`;
40
+ return { config: `${base}/${paths.config}`, templates: `${base}/${paths.templates}` };
41
+ }
42
+
19
43
  export function resolveSourceViewerPath(githubOwner, githubRepo) {
20
44
  // User repositories use the symbolic HEAD ref so any default branch works
21
45
  // (github.com, the trees API and raw.githubusercontent all accept it);
@@ -0,0 +1,16 @@
1
+ /** Options that shape the generated root config. */
2
+ export interface RootConfigOptions {
3
+ useLocalLayoutConfig?: boolean;
4
+ githubOwner?: string;
5
+ githubRepo?: string;
6
+ /** Repo-relative layouts home referenced by the generated config. */
7
+ layoutsDir?: string;
8
+ }
9
+
10
+ export interface RootConfig {
11
+ site: Record<string, unknown>;
12
+ VersionControl: { versions: Array<Record<string, unknown>> };
13
+ translations: Record<string, unknown>;
14
+ }
15
+
16
+ export declare function buildRootConfig(options?: RootConfigOptions): RootConfig;
@@ -1,6 +1,11 @@
1
1
  /** Build root config for gitpagedocs */
2
2
  import { OFFICIAL_LAYOUTS_CONFIG_URL, OFFICIAL_LAYOUTS_TEMPLATES_URL } from "../data/urls.mjs";
3
- import { resolveProjectLink, resolveRenderingUrl, resolveSourceViewerPath } from "./project-links.mjs";
3
+ import {
4
+ resolveProjectLink,
5
+ resolveRenderingUrl,
6
+ resolveSourceViewerPath,
7
+ resolveLayoutsLinks,
8
+ } from "./project-links.mjs";
4
9
  import { DOCS } from "../content/docs.mjs";
5
10
  import { DOC_VERSIONS } from "../data/version-constants.mjs";
6
11
  import { defaultLangMenu } from "../data/i18n-langmenu.mjs";
@@ -16,6 +21,7 @@ export function buildRootConfig(options = {}) {
16
21
  const renderingUrl = resolveRenderingUrl(githubOwner, githubRepo);
17
22
  const projectLink = resolveProjectLink(githubOwner, githubRepo);
18
23
  const sourceViewerPath = resolveSourceViewerPath(githubOwner, githubRepo);
24
+ const layoutsLinks = resolveLayoutsLinks(githubOwner, githubRepo, options.layoutsDir);
19
25
 
20
26
  const versionEntries = DOC_VERSIONS.map((id) => ({
21
27
  id,
@@ -39,6 +45,8 @@ export function buildRootConfig(options = {}) {
39
45
  layoutsConfigPathOficial: useOfficialLayouts,
40
46
  layoutsConfigPathTemplatesOficial: useOfficialLayouts ? OFFICIAL_LAYOUTS_TEMPLATES_URL : "",
41
47
  layoutsConfigPathOficialUrl: useOfficialLayouts ? OFFICIAL_LAYOUTS_CONFIG_URL : "",
48
+ layoutsConfigPath: useLocalLayoutConfig ? layoutsLinks.config : "",
49
+ layoutsConfigPathTemplates: useLocalLayoutConfig ? layoutsLinks.templates : "",
42
50
  repositorySearchHome,
43
51
  rendering: renderingUrl,
44
52
  AiChatEnabled: true,
@@ -21,8 +21,8 @@ The runtime expects this structure:
21
21
  - \`gitpagedocs/docs/<lang>/*.md\`
22
22
  - \`gitpagedocs/docs/versions/<version>/config.json\`
23
23
  - \`gitpagedocs/docs/versions/<version>/<lang>/*.md\`
24
- - \`gitpagedocs/layouts/layoutsConfig.json\`
25
- - \`gitpagedocs/layouts/templates/*.json\`
24
+ - \`gitpagelayouts/layoutsConfig.json\` (only with \`--layoutconfig\`)
25
+ - \`gitpagelayouts/templates/*.json\` (only with \`--layoutconfig\`)
26
26
 
27
27
  ## Quick navigation
28
28
 
@@ -113,10 +113,11 @@ Complete reference of CLI options, configuration keys, and runtime features.
113
113
  | Command | Description |
114
114
  |---------|--------------|
115
115
  | \`gitpagedocs\` | Generate config and docs in \`gitpagedocs/\` |
116
- | \`gitpagedocs --layoutconfig\` | Also generate local layouts/templates |
116
+ | \`gitpagedocs --layoutconfig\` | Also generate local layouts/templates in \`gitpagelayouts/\` |
117
117
  | \`gitpagedocs --home\` | Standalone distribution (\`gitpagedocshome/\`) |
118
118
  | \`gitpagedocs --push --owner X --repo Y\` | Setup workflow, commit, push |
119
- | \`gitpagedocs --interactive\` / \`-i\` | Interactive mode with prompts |
119
+ | \`gitpagedocs --interactive\` / \`-i\` | Interactive mode with prompts (the default in a terminal) |
120
+ | \`gitpagedocs --no-interactive\` / \`--yes\` / \`-y\` | Never prompt; use flags and defaults |
120
121
  | \`gitpagedocs ai\` | Interactive AI documentation generator |
121
122
  | \`gitpagedocs provider [id]\` / \`models [provider]\` | List AI providers / catalog models |
122
123
  | \`gitpagedocs document[:repo\\|:file\\|:folder]\` | Generate documentation with AI |
@@ -136,7 +137,8 @@ Install globally with \`npm install -g @gitpagedocs/cli\`, or run one-off with \
136
137
  | \`--path <subpath>\` | Docs subpath (e.g. \`docs\`); without it, base path = repo name for correct CSS/JS on project sites |
137
138
  | \`--output <dir>\` | Output directory (default: \`gitpagedocs\`) |
138
139
  | \`--search true|false\` | Enable/disable repository search (\`--home\`) |
139
- | \`--layoutconfig\` | Generate \`gitpagedocs/layouts/\` |
140
+ | \`--layoutconfig\` | Generate local layouts in \`gitpagelayouts/\` |
141
+ | \`--layouts-dir <dir>\` | Folder for local layouts (default: \`gitpagelayouts\`) |
140
142
  | \`--push\` | Create workflow, commit artifacts, push |
141
143
  | \`--home\` | Generate \`gitpagedocshome/\` (static + .env + Dockerfile) |
142
144
 
@@ -146,7 +148,7 @@ Install globally with \`npm install -g @gitpagedocs/cli\`, or run one-off with \
146
148
  - \`gitpagedocs/icon.svg\` – default icon
147
149
  - \`gitpagedocs/docs/versions/<ver>/config.json\` – per-version routes
148
150
  - \`gitpagedocs/docs/versions/<ver>/{en,pt,es}/*.md\` – markdown docs
149
- - \`gitpagedocs/layouts/\` – only with \`--layoutconfig\`
151
+ - \`gitpagelayouts/\` – only with \`--layoutconfig\` (folder configurable with \`--layouts-dir\`)
150
152
 
151
153
  ## Content types
152
154
 
@@ -490,9 +492,11 @@ Themes are JSON templates mapped by \`layoutsConfig.json\`.
490
492
 
491
493
  ## Local layout files
492
494
 
493
- - \`gitpagedocs/layouts/layoutsConfig.json\`
494
- - \`gitpagedocs/layouts/layoutsFallbackConfig.json\`
495
- - \`gitpagedocs/layouts/templates/*.json\`
495
+ - \`gitpagelayouts/layoutsConfig.json\`
496
+ - \`gitpagelayouts/layoutsFallbackConfig.json\`
497
+ - \`gitpagelayouts/templates/*.json\`
498
+
499
+ 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.
496
500
 
497
501
  ## Template model
498
502
 
@@ -19,8 +19,8 @@ El runtime espera esta estructura:
19
19
  - \`gitpagedocs/docs/<lang>/*.md\`
20
20
  - \`gitpagedocs/docs/versions/<version>/config.json\`
21
21
  - \`gitpagedocs/docs/versions/<version>/<lang>/*.md\`
22
- - \`gitpagedocs/layouts/layoutsConfig.json\`
23
- - \`gitpagedocs/layouts/templates/*.json\`
22
+ - \`gitpagelayouts/layoutsConfig.json\`
23
+ - \`gitpagelayouts/templates/*.json\`
24
24
 
25
25
  ## Navegacion rapida
26
26
 
@@ -106,10 +106,11 @@ Referencia completa de opciones CLI, claves de configuracion y funciones del run
106
106
  | Comando | Descripcion |
107
107
  |---------|-------------|
108
108
  | \`npx @gitpagedocs/cli\` | Genera config y docs en \`gitpagedocs/\` |
109
- | \`npx @gitpagedocs/cli --layoutconfig\` | Tambien genera layouts/templates locales |
109
+ | \`npx @gitpagedocs/cli --layoutconfig\` | Tambien genera layouts/templates locales en \`gitpagelayouts/\` |
110
110
  | \`npx @gitpagedocs/cli --home\` | Distribucion standalone (\`gitpagedocshome/\`) |
111
111
  | \`npx @gitpagedocs/cli --push --owner X --repo Y\` | Configura workflow, commit, push |
112
- | \`npx @gitpagedocs/cli --interactive\` / \`-i\` | Modo interactivo con prompts |
112
+ | \`npx @gitpagedocs/cli --interactive\` / \`-i\` | Modo interactivo con prompts (por defecto en una terminal) |
113
+ | \`npx @gitpagedocs/cli --no-interactive\` / \`-y\` | Nunca pregunta; usa flags y valores por defecto |
113
114
 
114
115
  ## Opciones CLI
115
116
 
@@ -120,7 +121,8 @@ Referencia completa de opciones CLI, claves de configuracion y funciones del run
120
121
  | \`--path <subpath>\` | Subruta de docs (ej: \`docs\`); sin ella, base path = nombre del repo para CSS/JS en project sites |
121
122
  | \`--output <dir>\` | Directorio de salida (default: \`gitpagedocs\`) |
122
123
  | \`--search true|false\` | Habilita/deshabilita busqueda de repositorio (\`--home\`) |
123
- | \`--layoutconfig\` | Genera \`gitpagedocs/layouts/\` |
124
+ | \`--layoutconfig\` | Genera layouts locales en \`gitpagelayouts/\` |
125
+ | \`--layouts-dir <dir>\` | Carpeta de layouts locales (por defecto: \`gitpagelayouts\`) |
124
126
  | \`--push\` | Crea workflow, commit de artefactos, push |
125
127
  | \`--home\` | Genera \`gitpagedocshome/\` (estatico + .env + Dockerfile) |
126
128
 
@@ -130,7 +132,7 @@ Referencia completa de opciones CLI, claves de configuracion y funciones del run
130
132
  - \`gitpagedocs/icon.svg\` – icono por defecto
131
133
  - \`gitpagedocs/docs/versions/<ver>/config.json\` – rutas por version
132
134
  - \`gitpagedocs/docs/versions/<ver>/{en,pt,es}/*.md\` – docs en markdown
133
- - \`gitpagedocs/layouts/\` – solo con \`--layoutconfig\`
135
+ - \`gitpagelayouts/\` – solo con \`--layoutconfig\` (carpeta configurable con \`--layouts-dir\`)
134
136
 
135
137
  ## Tipos de contenido
136
138
 
@@ -400,9 +402,9 @@ Los temas son templates JSON mapeados por \`layoutsConfig.json\`.
400
402
 
401
403
  ## Archivos
402
404
 
403
- - \`gitpagedocs/layouts/layoutsConfig.json\`
404
- - \`gitpagedocs/layouts/layoutsFallbackConfig.json\`
405
- - \`gitpagedocs/layouts/templates/*.json\`
405
+ - \`gitpagelayouts/layoutsConfig.json\`
406
+ - \`gitpagelayouts/layoutsFallbackConfig.json\`
407
+ - \`gitpagelayouts/templates/*.json\`
406
408
 
407
409
  ## Modelo de template
408
410
 
@@ -19,8 +19,8 @@ O runtime espera esta estrutura:
19
19
  - \`gitpagedocs/docs/<lang>/*.md\`
20
20
  - \`gitpagedocs/docs/versions/<versao>/config.json\`
21
21
  - \`gitpagedocs/docs/versions/<versao>/<lang>/*.md\`
22
- - \`gitpagedocs/layouts/layoutsConfig.json\`
23
- - \`gitpagedocs/layouts/templates/*.json\`
22
+ - \`gitpagelayouts/layoutsConfig.json\`
23
+ - \`gitpagelayouts/templates/*.json\`
24
24
 
25
25
  ## Navegacao rapida
26
26
 
@@ -106,10 +106,11 @@ Referencia completa de opcoes da CLI, chaves de configuracao e recursos do runti
106
106
  | Comando | Descricao |
107
107
  |---------|------------|
108
108
  | \`npx @gitpagedocs/cli\` | Gera config e docs em \`gitpagedocs/\` |
109
- | \`npx @gitpagedocs/cli --layoutconfig\` | Tambem gera layouts/templates locais |
109
+ | \`npx @gitpagedocs/cli --layoutconfig\` | Tambem gera layouts/templates locais em \`gitpagelayouts/\` |
110
110
  | \`npx @gitpagedocs/cli --home\` | Distribuicao standalone (\`gitpagedocshome/\`) |
111
111
  | \`npx @gitpagedocs/cli --push --owner X --repo Y\` | Configura workflow, commit, push |
112
- | \`npx @gitpagedocs/cli --interactive\` / \`-i\` | Modo interativo com prompts |
112
+ | \`npx @gitpagedocs/cli --interactive\` / \`-i\` | Modo interativo com prompts (padrao em um terminal) |
113
+ | \`npx @gitpagedocs/cli --no-interactive\` / \`-y\` | Nunca pergunta; usa flags e padroes |
113
114
 
114
115
  ## Opcoes da CLI
115
116
 
@@ -120,7 +121,8 @@ Referencia completa de opcoes da CLI, chaves de configuracao e recursos do runti
120
121
  | \`--path <subpath>\` | Subcaminho dos docs (ex: \`docs\`); sem ele, base path = nome do repo para CSS/JS em project sites |
121
122
  | \`--output <dir>\` | Diretorio de saida (padrao: \`gitpagedocs\`) |
122
123
  | \`--search true|false\` | Habilita/desabilita busca de repositorio (\`--home\`) |
123
- | \`--layoutconfig\` | Gera \`gitpagedocs/layouts/\` |
124
+ | \`--layoutconfig\` | Gera layouts locais em \`gitpagelayouts/\` |
125
+ | \`--layouts-dir <dir>\` | Pasta dos layouts locais (padrao: \`gitpagelayouts\`) |
124
126
  | \`--push\` | Cria workflow, commit de artefatos, push |
125
127
  | \`--home\` | Gera \`gitpagedocshome/\` (estatico + .env + Dockerfile) |
126
128
 
@@ -130,7 +132,7 @@ Referencia completa de opcoes da CLI, chaves de configuracao e recursos do runti
130
132
  - \`gitpagedocs/icon.svg\` – icone padrao
131
133
  - \`gitpagedocs/docs/versions/<ver>/config.json\` – rotas por versao
132
134
  - \`gitpagedocs/docs/versions/<ver>/{en,pt,es}/*.md\` – docs em markdown
133
- - \`gitpagedocs/layouts/\` – apenas com \`--layoutconfig\`
135
+ - \`gitpagelayouts/\` – apenas com \`--layoutconfig\` (pasta configuravel com \`--layouts-dir\`)
134
136
 
135
137
  ## Tipos de conteudo
136
138
 
@@ -400,9 +402,9 @@ Temas sao templates JSON mapeados por \`layoutsConfig.json\`.
400
402
 
401
403
  ## Arquivos
402
404
 
403
- - \`gitpagedocs/layouts/layoutsConfig.json\`
404
- - \`gitpagedocs/layouts/layoutsFallbackConfig.json\`
405
- - \`gitpagedocs/layouts/templates/*.json\`
405
+ - \`gitpagelayouts/layoutsConfig.json\`
406
+ - \`gitpagelayouts/layoutsFallbackConfig.json\`
407
+ - \`gitpagelayouts/templates/*.json\`
406
408
 
407
409
  ## Modelo de template
408
410
 
@@ -1,3 +1,3 @@
1
- export const PACKAGE_VERSION = "1.1.61";
1
+ export const PACKAGE_VERSION = "1.1.62";
2
2
 
3
3
  export const DOC_VERSIONS = [PACKAGE_VERSION];
@@ -1 +1,11 @@
1
1
  export { DOC_VERSIONS } from "./doc-versions.mjs";
2
+ export {
3
+ DEFAULT_LAYOUTS_DIR,
4
+ LAYOUTS_CONFIG_FILENAME,
5
+ LAYOUTS_FALLBACK_CONFIG_FILENAME,
6
+ LAYOUTS_TEMPLATES_DIRNAME,
7
+ LEGACY_LAYOUTS_SUBDIR,
8
+ normalizeLayoutsDir,
9
+ legacyLayoutsDir,
10
+ layoutsArtifactPaths,
11
+ } from "./layouts-paths.mjs";
@@ -0,0 +1,17 @@
1
+ /** Repo-relative paths owned by a layouts folder. */
2
+ export interface LayoutsArtifactPaths {
3
+ root: string;
4
+ config: string;
5
+ fallbackConfig: string;
6
+ templates: string;
7
+ }
8
+
9
+ export declare const DEFAULT_LAYOUTS_DIR: string;
10
+ export declare const LAYOUTS_CONFIG_FILENAME: string;
11
+ export declare const LAYOUTS_FALLBACK_CONFIG_FILENAME: string;
12
+ export declare const LAYOUTS_TEMPLATES_DIRNAME: string;
13
+ export declare const LEGACY_LAYOUTS_SUBDIR: string;
14
+
15
+ export declare function normalizeLayoutsDir(dir: string): string;
16
+ export declare function legacyLayoutsDir(outputDir: string): string;
17
+ export declare function layoutsArtifactPaths(layoutsDir: string): LayoutsArtifactPaths;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Canonical locations for layout artifacts.
3
+ *
4
+ * Local layouts are generated into the standalone `gitpagelayouts/` home at the
5
+ * project root. `<outputDir>/layouts/` is the legacy location that earlier
6
+ * versions wrote: the viewer still reads it, and the CLI offers to migrate it,
7
+ * but nothing is generated there any more.
8
+ *
9
+ * Every consumer derives its paths from here so the folder name exists in one
10
+ * place only.
11
+ */
12
+
13
+ /** Standalone layouts home, relative to the project root. */
14
+ export const DEFAULT_LAYOUTS_DIR = "gitpagelayouts";
15
+
16
+ /** Index file listing every layout, inside a layouts folder. */
17
+ export const LAYOUTS_CONFIG_FILENAME = "layoutsConfig.json";
18
+
19
+ /** Fallback index used when a layout id cannot be resolved. */
20
+ export const LAYOUTS_FALLBACK_CONFIG_FILENAME = "layoutsFallbackConfig.json";
21
+
22
+ /** Folder holding one JSON template per layout, inside a layouts folder. */
23
+ export const LAYOUTS_TEMPLATES_DIRNAME = "templates";
24
+
25
+ /** Sub-folder of the docs output dir that older versions generated into. */
26
+ export const LEGACY_LAYOUTS_SUBDIR = "layouts";
27
+
28
+ /**
29
+ * Normalize a user-supplied folder name to a repo-relative POSIX path with no
30
+ * leading/trailing separators, so it can be joined and stored in config.json
31
+ * identically on every platform.
32
+ *
33
+ * @param {string} dir Raw folder name (may use either separator).
34
+ * @returns {string} Normalized path, or `DEFAULT_LAYOUTS_DIR` when empty.
35
+ */
36
+ export function normalizeLayoutsDir(dir) {
37
+ const normalized = String(dir ?? "")
38
+ .replace(/[\\/]+/g, "/")
39
+ .replace(/^\/+|\/+$/g, "")
40
+ .trim();
41
+ return normalized || DEFAULT_LAYOUTS_DIR;
42
+ }
43
+
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
+ /**
55
+ * Repo-relative paths of every artifact a layouts folder owns.
56
+ *
57
+ * @param {string} layoutsDir Layouts folder, repo-relative.
58
+ * @returns {{ root: string, config: string, fallbackConfig: string, templates: string }}
59
+ */
60
+ export function layoutsArtifactPaths(layoutsDir) {
61
+ const root = normalizeLayoutsDir(layoutsDir);
62
+ return {
63
+ root,
64
+ config: `${root}/${LAYOUTS_CONFIG_FILENAME}`,
65
+ fallbackConfig: `${root}/${LAYOUTS_FALLBACK_CONFIG_FILENAME}`,
66
+ templates: `${root}/${LAYOUTS_TEMPLATES_DIRNAME}`,
67
+ };
68
+ }
@@ -1,11 +1,25 @@
1
1
  export type CliMode = "home" | "full" | "config-only" | "ai";
2
2
 
3
+ /**
4
+ * Values the user supplied explicitly on the command line. Interactive mode
5
+ * only asks for what is missing, so an explicit flag is never re-asked.
6
+ */
7
+ export interface CliExplicitFlags {
8
+ useLocalLayoutConfig: boolean;
9
+ layoutsDir: boolean;
10
+ githubOwner: boolean;
11
+ githubRepo: boolean;
12
+ outputDir: boolean;
13
+ }
14
+
3
15
  export interface CliOptions {
4
16
  isBuild: boolean;
5
17
  isServe: boolean;
6
18
  mode: CliMode;
7
19
  aiCommand?: string;
8
20
  outputDir: string;
21
+ /** Standalone layouts home (repo-relative); local layouts are generated here. */
22
+ layoutsDir: string;
9
23
  useLocalLayoutConfig: boolean;
10
24
  shouldPush: boolean;
11
25
  githubOwner: string;
@@ -15,4 +29,5 @@ export interface CliOptions {
15
29
  repositorySearch?: boolean;
16
30
  isInteractive: boolean;
17
31
  hasArgs: boolean;
32
+ explicit: CliExplicitFlags;
18
33
  }
@@ -0,0 +1,15 @@
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;
@@ -0,0 +1,60 @@
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();
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitpagedocs/cli",
3
- "version": "1.1.61",
3
+ "version": "1.1.62",
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.61",
33
- "@gitpagedocs/tools": "^1.1.61",
32
+ "@gitpagedocs/mcp": "^1.1.62",
33
+ "@gitpagedocs/tools": "^1.1.62",
34
34
  "tsx": "^4.21.0"
35
35
  },
36
36
  "repository": {
@@ -4,6 +4,7 @@ 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";
7
8
  import { runNewCommand } from "./commands/run-command";
8
9
  import { dispatchMode } from "../application/use-cases/dispatch-mode";
9
10
  import type {
@@ -57,6 +58,13 @@ async function main(): Promise<void> {
57
58
  }
58
59
 
59
60
  const options = await resolveOptions(process.argv, process.env);
61
+
62
+ // Layouts moved out of <outputDir>/layouts/. Offer to relocate anything left
63
+ // behind before generating, so the config written next points at one folder.
64
+ if (options.mode === "config-only") {
65
+ await migrateLegacyLayoutsInteractive(root, options);
66
+ }
67
+
60
68
  const configOnlyRuntime = createConfigOnlyRuntime() as ConfigOnlyRuntimePort;
61
69
  const homeRuntime = createHomeRuntime() as HomeRuntimePort;
62
70
  await dispatchMode(options, params, {
@@ -1,5 +1,6 @@
1
1
  import type { CliOptions } from "../../domain/models/cli-options";
2
2
  import { DEFAULTS } from "./schema";
3
+ import { normalizeLayoutsDir } from "../../contracts/layouts-paths.mjs";
3
4
 
4
5
  const KNOWN_FLAGS = new Set([
5
6
  "--build",
@@ -11,8 +12,12 @@ const KNOWN_FLAGS = new Set([
11
12
  "--search",
12
13
  "--path",
13
14
  "--output",
15
+ "--layouts-dir",
14
16
  "--interactive",
15
17
  "-i",
18
+ "--no-interactive",
19
+ "--yes",
20
+ "-y",
16
21
  "--ai",
17
22
  "--pages-actions",
18
23
  ]);
@@ -35,6 +40,7 @@ export function parseCliOptions(argv: string[], env: NodeJS.ProcessEnv): CliOpti
35
40
  let githubRepo = readOptionValue(args, "--repo");
36
41
  const docsPath = readOptionValue(args, "--path");
37
42
  const outputDir = readOptionValue(args, "--output");
43
+ const layoutsDir = readOptionValue(args, "--layouts-dir");
38
44
  const searchRaw = readOptionValue(args, "--search");
39
45
 
40
46
  const fallbackDashedArgs = args
@@ -45,6 +51,7 @@ export function parseCliOptions(argv: string[], env: NodeJS.ProcessEnv): CliOpti
45
51
  .filter((a) => !a.startsWith("--path"))
46
52
  .filter((a) => !a.startsWith("--home"))
47
53
  .filter((a) => !a.startsWith("--output"))
54
+ .filter((a) => !a.startsWith("--layouts-dir"))
48
55
  .filter((a) => !a.startsWith("--search"))
49
56
  .map((a) => a.replace(/^--/, "").trim())
50
57
  .filter(Boolean);
@@ -91,6 +98,7 @@ export function parseCliOptions(argv: string[], env: NodeJS.ProcessEnv): CliOpti
91
98
  mode,
92
99
  aiCommand,
93
100
  outputDir: outputDir || (isHome ? DEFAULTS.home.outputDir : DEFAULTS.outputDir),
101
+ layoutsDir: normalizeLayoutsDir(layoutsDir || DEFAULTS.layoutsDir),
94
102
  useLocalLayoutConfig,
95
103
  shouldPush,
96
104
  githubOwner,
@@ -100,5 +108,12 @@ export function parseCliOptions(argv: string[], env: NodeJS.ProcessEnv): CliOpti
100
108
  repositorySearch,
101
109
  isInteractive,
102
110
  hasArgs: args.length > 0,
111
+ explicit: {
112
+ useLocalLayoutConfig,
113
+ layoutsDir: Boolean(layoutsDir),
114
+ githubOwner: Boolean(githubOwner),
115
+ githubRepo: Boolean(githubRepo),
116
+ outputDir: Boolean(outputDir),
117
+ },
103
118
  };
104
119
  }
@@ -34,15 +34,13 @@ export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Pr
34
34
  return resolved;
35
35
  }
36
36
 
37
- const runHomeInteractive = parsed.isInteractive || (shouldRunInteractive(argv) && parsed.mode === "home");
38
- if (runHomeInteractive && parsed.mode === "home") {
39
- return promptHomeOptions(parsed);
40
- }
37
+ // Every command prompts in a real terminal; shouldRunInteractive owns the
38
+ // CI / piped-stdin / --no-interactive opt-outs, and each prompt asks only for
39
+ // what the command line left unanswered.
40
+ if (!shouldRunInteractive(argv)) return parsed;
41
41
 
42
- const runConfigInteractive = parsed.isInteractive || (shouldRunInteractive(argv) && parsed.mode === "config-only");
43
- if (runConfigInteractive && parsed.mode === "config-only") {
44
- return promptConfigOnlyOptions(parsed);
45
- }
42
+ if (parsed.mode === "home") return promptHomeOptions(parsed);
43
+ if (parsed.mode === "config-only") return promptConfigOnlyOptions(parsed);
46
44
 
47
45
  return parsed;
48
46
  }
@@ -1,5 +1,9 @@
1
+ import { DEFAULT_LAYOUTS_DIR } from "../../contracts/layouts-paths.mjs";
2
+
1
3
  export const DEFAULTS = {
2
4
  outputDir: "gitpagedocs",
5
+ /** Standalone layouts home; local layouts are generated here, never in outputDir. */
6
+ layoutsDir: DEFAULT_LAYOUTS_DIR as string,
3
7
  repositorySearch: false,
4
8
  basePath: "",
5
9
  home: {
@@ -0,0 +1,124 @@
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";
15
+
16
+ type LayoutsSource = "official" | "local";
17
+
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
+ /** Ask whether layouts come from the official remote source or a local folder. */
26
+ export async function askLayoutsSource(useLocal: boolean): Promise<boolean> {
27
+ const choice = await askSelect<LayoutsSource>(
28
+ "Layout source",
29
+ [
30
+ {
31
+ value: "official",
32
+ label: "Official layouts (remote)",
33
+ hint: "nothing is written to your repository",
34
+ },
35
+ {
36
+ value: "local",
37
+ label: "Local layouts",
38
+ hint: `generated into ${DEFAULT_LAYOUTS_DIR}/`,
39
+ },
40
+ ],
41
+ useLocal ? "local" : "official",
42
+ );
43
+ return choice === "local";
44
+ }
45
+
46
+ /** Ask which folder should hold the generated layouts. */
47
+ export async function askLayoutsDir(current: string): Promise<string> {
48
+ const answer = await askText({
49
+ message: "Layouts folder:",
50
+ defaultValue: normalizeLayoutsDir(current),
51
+ validate: (value) => (value.trim() ? undefined : "A folder name is required."),
52
+ });
53
+ return normalizeLayoutsDir(answer);
54
+ }
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
+ }
@@ -4,26 +4,19 @@ import { execSync } from "node:child_process";
4
4
  import type { CliOptions } from "../../domain/models/cli-options";
5
5
  import { DEFAULTS } from "../options/schema";
6
6
  import { askText, askConfirm, note } from "./clack";
7
+ import { interactivePromptsAvailable, promptingEnabled } from "./tty";
8
+ import { askLayoutsSource, askLayoutsDir } from "./layouts-prompts";
7
9
 
8
- function isCiOrNonTty(): boolean {
9
- if (process.env.CI === "true") return true;
10
- if (process.env.GITHUB_ACTIONS === "true") return true;
11
- if (process.stdin && !process.stdin.isTTY) return true;
12
- return false;
13
- }
14
-
15
- /** True when clack prompts can run (interactive TTY, not CI). */
16
- export function interactivePromptsAvailable(): boolean {
17
- return !isCiOrNonTty();
18
- }
10
+ export { interactivePromptsAvailable };
19
11
 
12
+ /**
13
+ * Every command prompts when it runs in a real terminal, so a bare invocation
14
+ * reflects what the user actually needs instead of silently taking defaults.
15
+ * CI, a piped stdin, and an explicit `--no-interactive` / `--yes` / `-y` opt out;
16
+ * `--interactive` / `-i` remains accepted and reads the same either way.
17
+ */
20
18
  export function shouldRunInteractive(argv: string[]): boolean {
21
- if (isCiOrNonTty()) return false;
22
- const args = argv.slice(2);
23
- if (args.includes("--interactive") || args.includes("-i")) return true;
24
- if (args.length === 0) return true;
25
- if (args.length === 1 && args[0] === "--home") return true;
26
- return false;
19
+ return promptingEnabled(argv.slice(2));
27
20
  }
28
21
 
29
22
  export async function promptHomeOptions(parsed: CliOptions): Promise<CliOptions> {
@@ -115,23 +108,38 @@ export async function ensureGitRepoInteractive(root: string): Promise<void> {
115
108
  }
116
109
  }
117
110
 
111
+ /**
112
+ * Ask only for what the command line did not already answer, so an explicit
113
+ * flag is never second-guessed by a prompt.
114
+ */
118
115
  export async function promptConfigOnlyOptions(parsed: CliOptions): Promise<CliOptions> {
119
- const useLocalLayoutConfig = await askConfirm(
120
- "Generate local layout templates?",
121
- parsed.useLocalLayoutConfig ?? false,
122
- );
123
- const githubOwner = await askText({
124
- message: "GitHub owner (optional):",
125
- defaultValue: parsed.githubOwner ?? "",
126
- });
127
- const githubRepo = await askText({
128
- message: "GitHub repo (optional):",
129
- defaultValue: parsed.githubRepo ?? "",
130
- });
116
+ const useLocalLayoutConfig = parsed.explicit.useLocalLayoutConfig
117
+ ? parsed.useLocalLayoutConfig
118
+ : await askLayoutsSource(parsed.useLocalLayoutConfig ?? false);
119
+
120
+ const layoutsDir =
121
+ useLocalLayoutConfig && !parsed.explicit.layoutsDir
122
+ ? await askLayoutsDir(parsed.layoutsDir)
123
+ : parsed.layoutsDir;
124
+
125
+ const githubOwner = parsed.explicit.githubOwner
126
+ ? parsed.githubOwner
127
+ : await askText({
128
+ message: "GitHub owner (optional):",
129
+ defaultValue: parsed.githubOwner ?? "",
130
+ });
131
+
132
+ const githubRepo = parsed.explicit.githubRepo
133
+ ? parsed.githubRepo
134
+ : await askText({
135
+ message: "GitHub repo (optional):",
136
+ defaultValue: parsed.githubRepo ?? "",
137
+ });
131
138
 
132
139
  return {
133
140
  ...parsed,
134
141
  useLocalLayoutConfig,
142
+ layoutsDir,
135
143
  githubOwner: githubOwner?.trim() ?? "",
136
144
  githubRepo: githubRepo?.trim() ?? "",
137
145
  };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Terminal capability detection.
3
+ *
4
+ * Kept in its own module so every prompt module can ask "may I prompt?" without
5
+ * importing the prompt modules themselves (which would be circular).
6
+ */
7
+
8
+ /** Environment variables that mark a non-interactive automated run. */
9
+ const CI_ENV_FLAGS = ["CI", "GITHUB_ACTIONS"] as const;
10
+
11
+ /** Flags that explicitly opt out of prompting. */
12
+ export const NON_INTERACTIVE_FLAGS = ["--no-interactive", "--yes", "-y"] as const;
13
+
14
+ function isCiOrNonTty(): boolean {
15
+ for (const flag of CI_ENV_FLAGS) {
16
+ if (process.env[flag] === "true") return true;
17
+ }
18
+ return Boolean(process.stdin && !process.stdin.isTTY);
19
+ }
20
+
21
+ /** True when clack prompts can run (interactive TTY, not CI). */
22
+ export function interactivePromptsAvailable(): boolean {
23
+ return !isCiOrNonTty();
24
+ }
25
+
26
+ /** True when the user explicitly asked for a non-interactive run. */
27
+ export function promptingOptedOut(args: readonly string[]): boolean {
28
+ return NON_INTERACTIVE_FLAGS.some((flag) => args.includes(flag));
29
+ }
30
+
31
+ /**
32
+ * True when prompts should run for this invocation: a real terminal, and no
33
+ * explicit opt-out.
34
+ */
35
+ export function promptingEnabled(args: readonly string[]): boolean {
36
+ return interactivePromptsAvailable() && !promptingOptedOut(args);
37
+ }
@@ -0,0 +1,11 @@
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;
@@ -0,0 +1,103 @@
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();
52
+ }
53
+
54
+ /**
55
+ * Move one file, falling back to copy + delete when rename is not possible
56
+ * (different volumes, or a locked handle on Windows).
57
+ *
58
+ * @param {string} fromAbsolute
59
+ * @param {string} toAbsolute
60
+ */
61
+ function moveFile(fromAbsolute, toAbsolute) {
62
+ mkdirSync(path.dirname(toAbsolute), { recursive: true });
63
+ try {
64
+ renameSync(fromAbsolute, toAbsolute);
65
+ } catch {
66
+ copyFileSync(fromAbsolute, toAbsolute);
67
+ rmSync(fromAbsolute, { force: true });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Execute a migration plan produced by `planLayoutsMigration`.
73
+ *
74
+ * Files are moved one by one so a single failure is reported without aborting
75
+ * the rest; the legacy folder is removed only once it is genuinely empty.
76
+ *
77
+ * @param {string} root Project root the plan's paths are relative to.
78
+ * @param {{ from: string, to: string, files: string[] }} plan
79
+ * @returns {{ moved: string[], failed: Array<{ file: string, reason: string }> }}
80
+ */
81
+ export function executeLayoutsMigration(root, plan) {
82
+ const moved = [];
83
+ const failed = [];
84
+
85
+ const fromRoot = path.join(root, ...plan.from.split("/"));
86
+ const toRoot = path.join(root, ...plan.to.split("/"));
87
+
88
+ for (const file of plan.files) {
89
+ const segments = file.split("/");
90
+ try {
91
+ moveFile(path.join(fromRoot, ...segments), path.join(toRoot, ...segments));
92
+ moved.push(file);
93
+ } catch (error) {
94
+ failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
95
+ }
96
+ }
97
+
98
+ if (failed.length === 0 && listFilesRecursively(fromRoot).length === 0) {
99
+ rmSync(fromRoot, { recursive: true, force: true });
100
+ }
101
+
102
+ return { moved, failed };
103
+ }
@@ -8,12 +8,7 @@ import {
8
8
  withVersionBadge,
9
9
  normalizeToOutputPath,
10
10
  } from "./doc-path-resolver.mjs";
11
-
12
- /** True when `child` is the same as, or nested inside, `parent` (path-safe). */
13
- function isWithin(parent, child) {
14
- const rel = path.relative(parent, child);
15
- return !rel.startsWith("..") && !path.isAbsolute(rel);
16
- }
11
+ import { layoutsArtifactPaths } from "../contracts/layouts-paths.mjs";
17
12
 
18
13
  const DEFAULT_ICON_SVG = `<?xml version="1.0" encoding="utf-8"?>
19
14
  <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -57,6 +52,29 @@ export async function writeText(root, relativePath, data) {
57
52
  await writeFile(absolutePath, data, "utf-8");
58
53
  }
59
54
 
55
+ /**
56
+ * Write the local layout artifacts into the standalone layouts home.
57
+ *
58
+ * No layouts folder is ever deleted here. The legacy `<outputDir>/layouts/`
59
+ * folder can hold hand-edited templates and the viewer still reads it, so
60
+ * relocating it is an explicit, confirmed migration rather than a side effect of
61
+ * generating. Switching back to official layouts likewise leaves an existing
62
+ * layouts home in place: `layoutsConfigPathOficial` already decides which source
63
+ * the viewer uses, so the files are inert rather than harmful.
64
+ */
65
+ async function writeLayoutArtifacts(options) {
66
+ const { root, layoutsDir, useLocalLayoutConfig, layouts, artifacts, createThemeTemplate } = options;
67
+
68
+ if (!useLocalLayoutConfig) return;
69
+
70
+ const paths = layoutsArtifactPaths(layoutsDir);
71
+ await writeJson(root, paths.config, artifacts.layoutsConfig);
72
+ await writeJson(root, paths.fallbackConfig, artifacts.fallbackLayoutsConfig);
73
+ for (const layout of layouts) {
74
+ await writeJson(root, `${paths.root}/${layout.file}`, createThemeTemplate(layout));
75
+ }
76
+ }
77
+
60
78
  export async function writeConfigOnlyOutput(options) {
61
79
  const {
62
80
  root,
@@ -64,6 +82,7 @@ export async function writeConfigOnlyOutput(options) {
64
82
  outputDir,
65
83
  artifacts,
66
84
  useLocalLayoutConfig,
85
+ layoutsDir,
67
86
  layouts,
68
87
  createThemeTemplate,
69
88
  } = options;
@@ -78,20 +97,14 @@ export async function writeConfigOnlyOutput(options) {
78
97
 
79
98
  await writeJson(root, `${outputDir}/config.json`, artifacts.rootConfig);
80
99
  await writeText(root, `${outputDir}/icon.svg`, DEFAULT_ICON_SVG);
81
- const layoutsPath = path.join(root, outputDir, "layouts");
82
- if (useLocalLayoutConfig) {
83
- await writeJson(root, `${outputDir}/layouts/layoutsConfig.json`, artifacts.layoutsConfig);
84
- await writeJson(root, `${outputDir}/layouts/layoutsFallbackConfig.json`, artifacts.fallbackLayoutsConfig);
85
- for (const layout of layouts) {
86
- const template = createThemeTemplate(layout);
87
- await writeJson(root, `${outputDir}/layouts/${layout.file}`, template);
88
- }
89
- } else if (existsSync(layoutsPath) && !isWithin(root, pkgRoot)) {
90
- // Only clean a stale local layouts/ when the CLI runs from OUTSIDE the
91
- // project (a real install) — never when pkgRoot is inside the project being
92
- // generated (the gitpagedocs source repo), so committed layouts survive.
93
- rmSync(layoutsPath, { recursive: true, force: true });
94
- }
100
+ await writeLayoutArtifacts({
101
+ root,
102
+ layoutsDir,
103
+ useLocalLayoutConfig,
104
+ layouts,
105
+ artifacts,
106
+ createThemeTemplate,
107
+ });
95
108
 
96
109
  for (const [versionId, versionConfig] of Object.entries(artifacts.versionConfigs)) {
97
110
  await writeJson(root, `${outputDir}/docs/versions/${versionId}/config.json`, versionConfig);